shokupan 0.1.0 → 0.3.0
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/README.md +1 -0
- package/dist/benchmarking/advanced-cases/elysia.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/express.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/fastify.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/hapi.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/hono.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/koa.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/nest.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/shokupan.d.ts +1 -0
- package/dist/benchmarking/advanced-data.d.ts +33 -0
- package/dist/benchmarking/advanced-runner.d.ts +1 -0
- package/dist/benchmarking/advanced-worker.d.ts +0 -0
- package/dist/benchmarking/cases/elysia.d.ts +1 -0
- package/dist/benchmarking/cases/express.d.ts +1 -0
- package/dist/benchmarking/cases/fastify.d.ts +1 -0
- package/dist/benchmarking/cases/hapi.d.ts +1 -0
- package/dist/benchmarking/cases/hono.d.ts +1 -0
- package/dist/benchmarking/cases/koa.d.ts +1 -0
- package/dist/benchmarking/cases/nest.d.ts +1 -0
- package/dist/benchmarking/cases/shokupan.d.ts +1 -0
- package/dist/benchmarking/data.d.ts +15 -0
- package/dist/benchmarking/quick_bench.d.ts +1 -0
- package/dist/benchmarking/runner.d.ts +1 -0
- package/dist/benchmarking/worker.d.ts +0 -0
- package/dist/buntest.d.ts +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +25 -8
- package/dist/decorators.d.ts +47 -0
- package/dist/index.cjs +1538 -655
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1532 -651
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +2 -0
- package/dist/{openapi-analyzer-cjdGeQ5a.js → openapi-analyzer-BtIaHIfe.js} +14 -6
- package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
- package/dist/{openapi-analyzer-CFqgSLNK.cjs → openapi-analyzer-D9YB3IkV.cjs} +14 -6
- package/dist/openapi-analyzer-D9YB3IkV.cjs.map +1 -0
- package/dist/plugins/auth.d.ts +1 -1
- package/dist/plugins/debugview/plugin.d.ts +28 -0
- package/dist/plugins/failed-request-recorder.d.ts +14 -0
- package/dist/plugins/idempotency/plugin.d.ts +14 -0
- package/dist/plugins/openapi-validator.d.ts +30 -0
- package/dist/plugins/proxy.d.ts +9 -0
- package/dist/plugins/rate-limit.d.ts +3 -1
- package/dist/plugins/serve-static.d.ts +2 -3
- package/dist/response.d.ts +4 -0
- package/dist/router/trie.d.ts +14 -0
- package/dist/router.d.ts +50 -3
- package/dist/server-adapter-BWrEJbKL.js +64 -0
- package/dist/server-adapter-BWrEJbKL.js.map +1 -0
- package/dist/server-adapter-fVKP60e0.cjs +81 -0
- package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
- package/dist/shokupan.d.ts +16 -3
- package/dist/types.d.ts +108 -4
- package/dist/util/cpu-monitor.d.ts +11 -0
- package/dist/util/stack.d.ts +8 -0
- package/package.json +8 -3
- package/dist/openapi-analyzer-CFqgSLNK.cjs.map +0 -1
- package/dist/openapi-analyzer-cjdGeQ5a.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
3
|
+
const promises = require("node:fs/promises");
|
|
4
4
|
const eta$2 = require("eta");
|
|
5
|
-
const promises = require("fs/promises");
|
|
5
|
+
const promises$1 = require("fs/promises");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const node_async_hooks = require("node:async_hooks");
|
|
8
|
+
const node = require("@surrealdb/node");
|
|
9
|
+
const surrealdb = require("surrealdb");
|
|
10
|
+
const api = require("@opentelemetry/api");
|
|
11
|
+
const os = require("node:os");
|
|
8
12
|
const arctic = require("arctic");
|
|
9
13
|
const jose = require("jose");
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
14
|
+
const zlib = require("node:zlib");
|
|
15
|
+
const Ajv = require("ajv");
|
|
16
|
+
const addFormats = require("ajv-formats");
|
|
13
17
|
const classTransformer = require("class-transformer");
|
|
14
18
|
const classValidator = require("class-validator");
|
|
19
|
+
const openapiAnalyzer = require("./openapi-analyzer-D9YB3IkV.cjs");
|
|
20
|
+
const crypto = require("crypto");
|
|
21
|
+
const events = require("events");
|
|
15
22
|
function _interopNamespaceDefault(e) {
|
|
16
23
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
17
24
|
if (e) {
|
|
@@ -28,14 +35,17 @@ function _interopNamespaceDefault(e) {
|
|
|
28
35
|
n.default = e;
|
|
29
36
|
return Object.freeze(n);
|
|
30
37
|
}
|
|
38
|
+
const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
|
|
31
39
|
const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
|
|
40
|
+
const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
|
|
32
41
|
class ShokupanResponse {
|
|
33
|
-
_headers =
|
|
42
|
+
_headers = null;
|
|
34
43
|
_status = 200;
|
|
35
44
|
/**
|
|
36
45
|
* Get the current headers
|
|
37
46
|
*/
|
|
38
47
|
get headers() {
|
|
48
|
+
if (!this._headers) this._headers = new Headers();
|
|
39
49
|
return this._headers;
|
|
40
50
|
}
|
|
41
51
|
/**
|
|
@@ -56,6 +66,7 @@ class ShokupanResponse {
|
|
|
56
66
|
* @param value Header value
|
|
57
67
|
*/
|
|
58
68
|
set(key, value) {
|
|
69
|
+
if (!this._headers) this._headers = new Headers();
|
|
59
70
|
this._headers.set(key, value);
|
|
60
71
|
return this;
|
|
61
72
|
}
|
|
@@ -65,6 +76,7 @@ class ShokupanResponse {
|
|
|
65
76
|
* @param value Header value
|
|
66
77
|
*/
|
|
67
78
|
append(key, value) {
|
|
79
|
+
if (!this._headers) this._headers = new Headers();
|
|
68
80
|
this._headers.append(key, value);
|
|
69
81
|
return this;
|
|
70
82
|
}
|
|
@@ -73,29 +85,62 @@ class ShokupanResponse {
|
|
|
73
85
|
* @param key Header name
|
|
74
86
|
*/
|
|
75
87
|
get(key) {
|
|
76
|
-
return this._headers
|
|
88
|
+
return this._headers?.get(key) || null;
|
|
77
89
|
}
|
|
78
90
|
/**
|
|
79
91
|
* Check if a header exists
|
|
80
92
|
* @param key Header name
|
|
81
93
|
*/
|
|
82
94
|
has(key) {
|
|
83
|
-
return this._headers
|
|
95
|
+
return this._headers?.has(key) || false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Internal: check if headers have been initialized/modified
|
|
99
|
+
*/
|
|
100
|
+
get hasPopulatedHeaders() {
|
|
101
|
+
return this._headers !== null;
|
|
84
102
|
}
|
|
85
103
|
}
|
|
86
104
|
class ShokupanContext {
|
|
87
|
-
|
|
105
|
+
// Raw body for compression optimization
|
|
106
|
+
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
88
107
|
this.request = request;
|
|
89
108
|
this.server = server;
|
|
90
109
|
this.app = app;
|
|
91
|
-
this.
|
|
110
|
+
this.signal = signal;
|
|
92
111
|
this.state = state || {};
|
|
112
|
+
if (enableMiddlewareTracking) {
|
|
113
|
+
const self = this;
|
|
114
|
+
this.state = new Proxy(this.state, {
|
|
115
|
+
set(target, p, newValue, receiver) {
|
|
116
|
+
const result = Reflect.set(target, p, newValue, receiver);
|
|
117
|
+
const currentHandler = self.handlerStack[self.handlerStack.length - 1];
|
|
118
|
+
if (currentHandler) {
|
|
119
|
+
if (!currentHandler.stateChanges) currentHandler.stateChanges = {};
|
|
120
|
+
currentHandler.stateChanges[p] = newValue;
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
93
126
|
this.response = new ShokupanResponse();
|
|
94
127
|
}
|
|
95
|
-
|
|
128
|
+
_url;
|
|
96
129
|
params = {};
|
|
130
|
+
// Router assigns this, but default to empty object
|
|
97
131
|
state;
|
|
132
|
+
handlerStack = [];
|
|
98
133
|
response;
|
|
134
|
+
_debug;
|
|
135
|
+
_finalResponse;
|
|
136
|
+
_rawBody;
|
|
137
|
+
get url() {
|
|
138
|
+
if (!this._url) {
|
|
139
|
+
const urlString = this.request.url || "http://localhost/";
|
|
140
|
+
this._url = new URL(urlString);
|
|
141
|
+
}
|
|
142
|
+
return this._url;
|
|
143
|
+
}
|
|
99
144
|
/**
|
|
100
145
|
* Base request
|
|
101
146
|
*/
|
|
@@ -112,13 +157,42 @@ class ShokupanContext {
|
|
|
112
157
|
* Request path
|
|
113
158
|
*/
|
|
114
159
|
get path() {
|
|
115
|
-
return this.
|
|
160
|
+
if (this._url) return this._url.pathname;
|
|
161
|
+
const url = this.request.url;
|
|
162
|
+
let queryIndex = url.indexOf("?");
|
|
163
|
+
const end = queryIndex === -1 ? url.length : queryIndex;
|
|
164
|
+
let start = 0;
|
|
165
|
+
const protocolIndex = url.indexOf("://");
|
|
166
|
+
if (protocolIndex !== -1) {
|
|
167
|
+
const hostStart = protocolIndex + 3;
|
|
168
|
+
const pathStart = url.indexOf("/", hostStart);
|
|
169
|
+
if (pathStart !== -1 && pathStart < end) {
|
|
170
|
+
start = pathStart;
|
|
171
|
+
} else {
|
|
172
|
+
return "/";
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
if (url.charCodeAt(0) === 47) {
|
|
176
|
+
start = 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return url.substring(start, end);
|
|
116
180
|
}
|
|
117
181
|
/**
|
|
118
182
|
* Request query params
|
|
119
183
|
*/
|
|
120
184
|
get query() {
|
|
121
|
-
|
|
185
|
+
const q = {};
|
|
186
|
+
for (const [key, value] of this.url.searchParams) {
|
|
187
|
+
if (q[key] === void 0) {
|
|
188
|
+
q[key] = value;
|
|
189
|
+
} else if (Array.isArray(q[key])) {
|
|
190
|
+
q[key].push(value);
|
|
191
|
+
} else {
|
|
192
|
+
q[key] = [q[key], value];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return q;
|
|
122
196
|
}
|
|
123
197
|
/**
|
|
124
198
|
* Client IP address
|
|
@@ -193,25 +267,60 @@ class ShokupanContext {
|
|
|
193
267
|
setCookie(name, value, options = {}) {
|
|
194
268
|
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
195
269
|
if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
270
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
271
|
+
if (options.path) cookie += `; Path=${options.path || "/"}`;
|
|
196
272
|
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
197
273
|
if (options.httpOnly) cookie += `; HttpOnly`;
|
|
198
274
|
if (options.secure) cookie += `; Secure`;
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
275
|
+
let sameSite = options.sameSite;
|
|
276
|
+
if (sameSite === true) sameSite = "Strict";
|
|
277
|
+
if (sameSite === void 0 || sameSite === false) ;
|
|
278
|
+
else {
|
|
279
|
+
const stringSameSite = typeof sameSite === "string" ? sameSite.toLowerCase() : sameSite;
|
|
280
|
+
switch (stringSameSite) {
|
|
281
|
+
case "lax":
|
|
282
|
+
cookie += "; SameSite=Lax";
|
|
283
|
+
break;
|
|
284
|
+
case "strict":
|
|
285
|
+
cookie += "; SameSite=Strict";
|
|
286
|
+
break;
|
|
287
|
+
case "none":
|
|
288
|
+
cookie += "; SameSite=None";
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
cookie += "; SameSite=Lax";
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
204
294
|
}
|
|
205
295
|
if (options.priority) {
|
|
206
|
-
|
|
296
|
+
const p = options.priority.toLowerCase();
|
|
297
|
+
if (p === "low") cookie += "; Priority=Low";
|
|
298
|
+
else if (p === "medium") cookie += "; Priority=Medium";
|
|
299
|
+
else if (p === "high") cookie += "; Priority=High";
|
|
207
300
|
}
|
|
208
301
|
this.response.append("Set-Cookie", cookie);
|
|
209
302
|
return this;
|
|
210
303
|
}
|
|
211
304
|
mergeHeaders(headers) {
|
|
212
|
-
|
|
305
|
+
let h;
|
|
306
|
+
if (this.response.hasPopulatedHeaders) {
|
|
307
|
+
h = new Headers(this.response.headers);
|
|
308
|
+
} else {
|
|
309
|
+
h = new Headers();
|
|
310
|
+
}
|
|
213
311
|
if (headers) {
|
|
214
|
-
|
|
312
|
+
if (headers instanceof Headers) {
|
|
313
|
+
headers.forEach((v, k) => h.set(k, v));
|
|
314
|
+
} else if (Array.isArray(headers)) {
|
|
315
|
+
headers.forEach(([k, v]) => h.set(k, v));
|
|
316
|
+
} else {
|
|
317
|
+
const keys = Object.keys(headers);
|
|
318
|
+
for (let i = 0; i < keys.length; i++) {
|
|
319
|
+
const key = keys[i];
|
|
320
|
+
const val = headers[key];
|
|
321
|
+
h.set(key, val);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
215
324
|
}
|
|
216
325
|
return h;
|
|
217
326
|
}
|
|
@@ -224,17 +333,21 @@ class ShokupanContext {
|
|
|
224
333
|
send(body, options) {
|
|
225
334
|
const headers = this.mergeHeaders(options?.headers);
|
|
226
335
|
const status = options?.status ?? this.response.status;
|
|
227
|
-
|
|
336
|
+
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
337
|
+
this._rawBody = body;
|
|
338
|
+
}
|
|
339
|
+
this._finalResponse = new Response(body, { status, headers });
|
|
340
|
+
return this._finalResponse;
|
|
228
341
|
}
|
|
229
342
|
/**
|
|
230
343
|
* Read request body
|
|
231
344
|
*/
|
|
232
345
|
async body() {
|
|
233
|
-
const contentType = this.request.headers.get("content-type");
|
|
234
|
-
if (contentType
|
|
346
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
347
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
235
348
|
return this.request.json();
|
|
236
349
|
}
|
|
237
|
-
if (contentType
|
|
350
|
+
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
238
351
|
return this.request.formData();
|
|
239
352
|
}
|
|
240
353
|
return this.request.text();
|
|
@@ -243,28 +356,49 @@ class ShokupanContext {
|
|
|
243
356
|
* Respond with a JSON object
|
|
244
357
|
*/
|
|
245
358
|
json(data, status, headers) {
|
|
359
|
+
const finalStatus = status ?? this.response.status;
|
|
360
|
+
const jsonString = JSON.stringify(data);
|
|
361
|
+
this._rawBody = jsonString;
|
|
362
|
+
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
363
|
+
this._finalResponse = new Response(jsonString, {
|
|
364
|
+
status: finalStatus,
|
|
365
|
+
headers: { "content-type": "application/json" }
|
|
366
|
+
});
|
|
367
|
+
return this._finalResponse;
|
|
368
|
+
}
|
|
246
369
|
const finalHeaders = this.mergeHeaders(headers);
|
|
247
370
|
finalHeaders.set("content-type", "application/json");
|
|
248
|
-
|
|
249
|
-
return
|
|
371
|
+
this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
|
|
372
|
+
return this._finalResponse;
|
|
250
373
|
}
|
|
251
374
|
/**
|
|
252
375
|
* Respond with a text string
|
|
253
376
|
*/
|
|
254
377
|
text(data, status, headers) {
|
|
255
|
-
const finalHeaders = this.mergeHeaders(headers);
|
|
256
|
-
finalHeaders.set("content-type", "text/plain");
|
|
257
378
|
const finalStatus = status ?? this.response.status;
|
|
258
|
-
|
|
379
|
+
this._rawBody = data;
|
|
380
|
+
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
381
|
+
this._finalResponse = new Response(data, {
|
|
382
|
+
status: finalStatus,
|
|
383
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
384
|
+
});
|
|
385
|
+
return this._finalResponse;
|
|
386
|
+
}
|
|
387
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
388
|
+
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
389
|
+
this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
390
|
+
return this._finalResponse;
|
|
259
391
|
}
|
|
260
392
|
/**
|
|
261
393
|
* Respond with HTML content
|
|
262
394
|
*/
|
|
263
395
|
html(html, status, headers) {
|
|
264
|
-
const finalHeaders = this.mergeHeaders(headers);
|
|
265
|
-
finalHeaders.set("content-type", "text/html");
|
|
266
396
|
const finalStatus = status ?? this.response.status;
|
|
267
|
-
|
|
397
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
398
|
+
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
399
|
+
this._rawBody = html;
|
|
400
|
+
this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
401
|
+
return this._finalResponse;
|
|
268
402
|
}
|
|
269
403
|
/**
|
|
270
404
|
* Respond with a redirect
|
|
@@ -272,7 +406,8 @@ class ShokupanContext {
|
|
|
272
406
|
redirect(url, status = 302) {
|
|
273
407
|
const headers = this.mergeHeaders();
|
|
274
408
|
headers.set("Location", url);
|
|
275
|
-
|
|
409
|
+
this._finalResponse = new Response(null, { status, headers });
|
|
410
|
+
return this._finalResponse;
|
|
276
411
|
}
|
|
277
412
|
/**
|
|
278
413
|
* Respond with a status code
|
|
@@ -280,15 +415,26 @@ class ShokupanContext {
|
|
|
280
415
|
*/
|
|
281
416
|
status(status) {
|
|
282
417
|
const headers = this.mergeHeaders();
|
|
283
|
-
|
|
418
|
+
this._finalResponse = new Response(null, { status, headers });
|
|
419
|
+
return this._finalResponse;
|
|
284
420
|
}
|
|
285
421
|
/**
|
|
286
422
|
* Respond with a file
|
|
287
423
|
*/
|
|
288
|
-
file(path2, fileOptions, responseOptions) {
|
|
424
|
+
async file(path2, fileOptions, responseOptions) {
|
|
289
425
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
290
426
|
const status = responseOptions?.status ?? this.response.status;
|
|
291
|
-
|
|
427
|
+
if (typeof Bun !== "undefined") {
|
|
428
|
+
this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
429
|
+
return this._finalResponse;
|
|
430
|
+
} else {
|
|
431
|
+
const fileBuffer = await promises.readFile(path2);
|
|
432
|
+
if (fileOptions?.type) {
|
|
433
|
+
headers.set("content-type", fileOptions.type);
|
|
434
|
+
}
|
|
435
|
+
this._finalResponse = new Response(fileBuffer, { status, headers });
|
|
436
|
+
return this._finalResponse;
|
|
437
|
+
}
|
|
292
438
|
}
|
|
293
439
|
/**
|
|
294
440
|
* JSX Rendering Function
|
|
@@ -308,6 +454,74 @@ class ShokupanContext {
|
|
|
308
454
|
return this.html(html, status, headers);
|
|
309
455
|
}
|
|
310
456
|
}
|
|
457
|
+
function RateLimitMiddleware(options = {}) {
|
|
458
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
459
|
+
const max = options.limit || options.max || 5;
|
|
460
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
461
|
+
const statusCode = options.statusCode || 429;
|
|
462
|
+
const headers = options.headers !== false;
|
|
463
|
+
const mode = options.mode || "user";
|
|
464
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
465
|
+
if (mode === "absolute") {
|
|
466
|
+
return "global";
|
|
467
|
+
}
|
|
468
|
+
return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
469
|
+
});
|
|
470
|
+
const skip = options.skip || (() => false);
|
|
471
|
+
const hits = /* @__PURE__ */ new Map();
|
|
472
|
+
const interval = setInterval(() => {
|
|
473
|
+
const now = Date.now();
|
|
474
|
+
for (const [key, record] of hits.entries()) {
|
|
475
|
+
if (record.resetTime <= now) {
|
|
476
|
+
hits.delete(key);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}, windowMs);
|
|
480
|
+
if (interval.unref) interval.unref();
|
|
481
|
+
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
482
|
+
if (skip(ctx)) return next();
|
|
483
|
+
const key = keyGenerator(ctx);
|
|
484
|
+
const now = Date.now();
|
|
485
|
+
let record = hits.get(key);
|
|
486
|
+
if (!record || record.resetTime <= now) {
|
|
487
|
+
record = {
|
|
488
|
+
hits: 0,
|
|
489
|
+
resetTime: now + windowMs
|
|
490
|
+
};
|
|
491
|
+
hits.set(key, record);
|
|
492
|
+
}
|
|
493
|
+
record.hits++;
|
|
494
|
+
const remaining = Math.max(0, max - record.hits);
|
|
495
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
496
|
+
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
497
|
+
const setHeaders = (res) => {
|
|
498
|
+
if (!headers || !res || !res.headers) return;
|
|
499
|
+
try {
|
|
500
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
501
|
+
res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
502
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
503
|
+
} catch (e) {
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
if (record.hits > max) {
|
|
507
|
+
typeof message === "object" ? JSON.stringify(message) : String(message);
|
|
508
|
+
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
509
|
+
if (headers) {
|
|
510
|
+
setHeaders(res);
|
|
511
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
512
|
+
}
|
|
513
|
+
return res;
|
|
514
|
+
}
|
|
515
|
+
const response = await next();
|
|
516
|
+
if (response instanceof Response && headers) {
|
|
517
|
+
setHeaders(response);
|
|
518
|
+
}
|
|
519
|
+
return response;
|
|
520
|
+
};
|
|
521
|
+
rateLimitMiddleware.isBuiltin = true;
|
|
522
|
+
rateLimitMiddleware.pluginName = "RateLimit";
|
|
523
|
+
return rateLimitMiddleware;
|
|
524
|
+
}
|
|
311
525
|
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
312
526
|
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
313
527
|
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
@@ -404,6 +618,9 @@ const Patch = createMethodDecorator("PATCH");
|
|
|
404
618
|
const Options = createMethodDecorator("OPTIONS");
|
|
405
619
|
const Head = createMethodDecorator("HEAD");
|
|
406
620
|
const All = createMethodDecorator("ALL");
|
|
621
|
+
function RateLimit(options) {
|
|
622
|
+
return Use(RateLimitMiddleware(options));
|
|
623
|
+
}
|
|
407
624
|
class Container {
|
|
408
625
|
static services = /* @__PURE__ */ new Map();
|
|
409
626
|
static register(target, instance) {
|
|
@@ -437,69 +654,43 @@ function Inject(token) {
|
|
|
437
654
|
});
|
|
438
655
|
};
|
|
439
656
|
}
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
function traceHandler(fn, name) {
|
|
465
|
-
return async function(...args) {
|
|
466
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
467
|
-
kind: api.SpanKind.INTERNAL,
|
|
468
|
-
attributes: {
|
|
469
|
-
"http.route": name,
|
|
470
|
-
"component": "shokupan.route"
|
|
471
|
-
}
|
|
472
|
-
}, async (span) => {
|
|
657
|
+
const compose = (middleware) => {
|
|
658
|
+
if (!middleware.length) {
|
|
659
|
+
return (context, next) => {
|
|
660
|
+
return next ? next() : Promise.resolve();
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
return function dispatch(context, next) {
|
|
664
|
+
let index = -1;
|
|
665
|
+
async function runner(i) {
|
|
666
|
+
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
667
|
+
index = i;
|
|
668
|
+
if (i >= middleware.length) {
|
|
669
|
+
return next ? next() : Promise.resolve();
|
|
670
|
+
}
|
|
671
|
+
const fn = middleware[i];
|
|
672
|
+
if (!context._debug) {
|
|
673
|
+
return fn(context, () => runner(i + 1));
|
|
674
|
+
}
|
|
675
|
+
const debug = context._debug;
|
|
676
|
+
const debugId = fn._debugId || fn.name || "anonymous";
|
|
677
|
+
const previousNode = debug.getCurrentNode();
|
|
678
|
+
debug.trackEdge(previousNode, debugId);
|
|
679
|
+
debug.setNode(debugId);
|
|
680
|
+
const start = performance.now();
|
|
473
681
|
try {
|
|
474
|
-
const
|
|
475
|
-
|
|
682
|
+
const res = await Promise.resolve(fn(context, () => runner(i + 1)));
|
|
683
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "success");
|
|
684
|
+
return res;
|
|
476
685
|
} catch (err) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
throw err;
|
|
686
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
|
|
687
|
+
return Promise.reject(err);
|
|
480
688
|
} finally {
|
|
481
|
-
|
|
689
|
+
if (previousNode) debug.setNode(previousNode);
|
|
482
690
|
}
|
|
483
|
-
});
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
const compose = (middleware) => {
|
|
487
|
-
function fn(context, next) {
|
|
488
|
-
let runner = next || (async () => {
|
|
489
|
-
});
|
|
490
|
-
for (let i = middleware.length - 1; i >= 0; i--) {
|
|
491
|
-
const fn2 = traceMiddleware(middleware[i]);
|
|
492
|
-
const nextStep = runner;
|
|
493
|
-
let called = false;
|
|
494
|
-
runner = async () => {
|
|
495
|
-
if (called) throw new Error("next() called multiple times");
|
|
496
|
-
called = true;
|
|
497
|
-
return fn2(context, nextStep);
|
|
498
|
-
};
|
|
499
691
|
}
|
|
500
|
-
return runner();
|
|
501
|
-
}
|
|
502
|
-
return fn;
|
|
692
|
+
return runner(0);
|
|
693
|
+
};
|
|
503
694
|
};
|
|
504
695
|
class ShokupanRequestBase {
|
|
505
696
|
method;
|
|
@@ -557,6 +748,15 @@ function deepMerge(target, ...sources) {
|
|
|
557
748
|
}
|
|
558
749
|
return deepMerge(target, ...sources);
|
|
559
750
|
}
|
|
751
|
+
const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
|
|
752
|
+
const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
|
|
753
|
+
const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
|
|
754
|
+
const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
|
|
755
|
+
const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
|
|
756
|
+
const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
|
|
757
|
+
const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
|
|
758
|
+
const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
|
|
759
|
+
const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
|
|
560
760
|
function analyzeHandler(handler) {
|
|
561
761
|
const handlerSource = handler.toString();
|
|
562
762
|
const inferredSpec = {};
|
|
@@ -566,46 +766,28 @@ function analyzeHandler(handler) {
|
|
|
566
766
|
};
|
|
567
767
|
}
|
|
568
768
|
const queryParams = /* @__PURE__ */ new Map();
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
queryIntMatch.forEach((match) => {
|
|
572
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
573
|
-
if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
|
|
574
|
-
});
|
|
769
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
|
|
770
|
+
if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
|
|
575
771
|
}
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
queryFloatMatch.forEach((match) => {
|
|
579
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
580
|
-
if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
|
|
581
|
-
});
|
|
772
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
|
|
773
|
+
if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
|
|
582
774
|
}
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (paramName && !queryParams.has(paramName)) {
|
|
588
|
-
queryParams.set(paramName, { type: "number" });
|
|
589
|
-
}
|
|
590
|
-
});
|
|
775
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
|
|
776
|
+
if (match[1] && !queryParams.has(match[1])) {
|
|
777
|
+
queryParams.set(match[1], { type: "number" });
|
|
778
|
+
}
|
|
591
779
|
}
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
queryParams.set(paramName, { type: "boolean" });
|
|
598
|
-
}
|
|
599
|
-
});
|
|
780
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
|
|
781
|
+
const name = match[1] || match[2];
|
|
782
|
+
if (name && !queryParams.has(name)) {
|
|
783
|
+
queryParams.set(name, { type: "boolean" });
|
|
784
|
+
}
|
|
600
785
|
}
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
queryParams.set(paramName, { type: "string" });
|
|
607
|
-
}
|
|
608
|
-
});
|
|
786
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
|
|
787
|
+
const name = match[1];
|
|
788
|
+
if (name && !queryParams.has(name)) {
|
|
789
|
+
queryParams.set(name, { type: "string" });
|
|
790
|
+
}
|
|
609
791
|
}
|
|
610
792
|
if (queryParams.size > 0) {
|
|
611
793
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -618,19 +800,11 @@ function analyzeHandler(handler) {
|
|
|
618
800
|
});
|
|
619
801
|
}
|
|
620
802
|
const pathParams = /* @__PURE__ */ new Map();
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
paramIntMatch.forEach((match) => {
|
|
624
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
625
|
-
if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
|
|
626
|
-
});
|
|
803
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
|
|
804
|
+
if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
|
|
627
805
|
}
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
paramFloatMatch.forEach((match) => {
|
|
631
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
632
|
-
if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
|
|
633
|
-
});
|
|
806
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
|
|
807
|
+
if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
|
|
634
808
|
}
|
|
635
809
|
if (pathParams.size > 0) {
|
|
636
810
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -643,76 +817,55 @@ function analyzeHandler(handler) {
|
|
|
643
817
|
});
|
|
644
818
|
});
|
|
645
819
|
}
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
schema: { type: "string" }
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
});
|
|
820
|
+
for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
|
|
821
|
+
if (match[1]) {
|
|
822
|
+
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
823
|
+
inferredSpec.parameters.push({
|
|
824
|
+
name: match[1],
|
|
825
|
+
in: "header",
|
|
826
|
+
schema: { type: "string" }
|
|
827
|
+
});
|
|
828
|
+
}
|
|
659
829
|
}
|
|
660
830
|
const responses = {};
|
|
661
831
|
if (handlerSource.includes("ctx.json(")) {
|
|
662
832
|
responses["200"] = {
|
|
663
833
|
description: "Successful response",
|
|
664
|
-
content: {
|
|
665
|
-
"application/json": { schema: { type: "object" } }
|
|
666
|
-
}
|
|
834
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
667
835
|
};
|
|
668
836
|
}
|
|
669
837
|
if (handlerSource.includes("ctx.html(")) {
|
|
670
838
|
responses["200"] = {
|
|
671
839
|
description: "Successful response",
|
|
672
|
-
content: {
|
|
673
|
-
"text/html": { schema: { type: "string" } }
|
|
674
|
-
}
|
|
840
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
675
841
|
};
|
|
676
842
|
}
|
|
677
843
|
if (handlerSource.includes("ctx.text(")) {
|
|
678
844
|
responses["200"] = {
|
|
679
845
|
description: "Successful response",
|
|
680
|
-
content: {
|
|
681
|
-
"text/plain": { schema: { type: "string" } }
|
|
682
|
-
}
|
|
846
|
+
content: { "text/plain": { schema: { type: "string" } } }
|
|
683
847
|
};
|
|
684
848
|
}
|
|
685
849
|
if (handlerSource.includes("ctx.file(")) {
|
|
686
850
|
responses["200"] = {
|
|
687
851
|
description: "File download",
|
|
688
|
-
content: {
|
|
689
|
-
"application/octet-stream": { schema: { type: "string", format: "binary" } }
|
|
690
|
-
}
|
|
852
|
+
content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
|
|
691
853
|
};
|
|
692
854
|
}
|
|
693
855
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
694
|
-
responses["302"] = {
|
|
695
|
-
description: "Redirect"
|
|
696
|
-
};
|
|
856
|
+
responses["302"] = { description: "Redirect" };
|
|
697
857
|
}
|
|
698
858
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
699
859
|
responses["200"] = {
|
|
700
860
|
description: "Successful response",
|
|
701
|
-
content: {
|
|
702
|
-
"application/json": { schema: { type: "object" } }
|
|
703
|
-
}
|
|
861
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
704
862
|
};
|
|
705
863
|
}
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
responses[statusCode] = {
|
|
712
|
-
description: `Error response (${statusCode})`
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
});
|
|
864
|
+
for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
|
|
865
|
+
const statusCode = match[1];
|
|
866
|
+
if (statusCode && statusCode !== "200") {
|
|
867
|
+
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
868
|
+
}
|
|
716
869
|
}
|
|
717
870
|
if (Object.keys(responses).length > 0) {
|
|
718
871
|
inferredSpec.responses = responses;
|
|
@@ -726,7 +879,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
726
879
|
const defaultTagName = options.defaultTag || "Application";
|
|
727
880
|
let astRoutes = [];
|
|
728
881
|
try {
|
|
729
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-
|
|
882
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-D9YB3IkV.cjs"));
|
|
730
883
|
const analyzer = new OpenAPIAnalyzer(process.cwd());
|
|
731
884
|
const { applications } = await analyzer.analyze();
|
|
732
885
|
const appMap = /* @__PURE__ */ new Map();
|
|
@@ -996,10 +1149,10 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
996
1149
|
};
|
|
997
1150
|
}
|
|
998
1151
|
const eta$1 = new eta$2.Eta();
|
|
999
|
-
function serveStatic(
|
|
1152
|
+
function serveStatic(config, prefix) {
|
|
1000
1153
|
const rootPath = path.resolve(config.root || ".");
|
|
1001
1154
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1002
|
-
|
|
1155
|
+
const serveStaticMiddleware = async (ctx) => {
|
|
1003
1156
|
let relative = ctx.path.slice(normalizedPrefix.length);
|
|
1004
1157
|
if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
|
|
1005
1158
|
if (relative.length === 0) relative = "/";
|
|
@@ -1032,13 +1185,13 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1032
1185
|
let finalPath = requestPath;
|
|
1033
1186
|
let stats;
|
|
1034
1187
|
try {
|
|
1035
|
-
stats = await promises.stat(requestPath);
|
|
1188
|
+
stats = await promises$1.stat(requestPath);
|
|
1036
1189
|
} catch (e) {
|
|
1037
1190
|
if (config.extensions) {
|
|
1038
1191
|
for (const ext of config.extensions) {
|
|
1039
1192
|
const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
|
|
1040
1193
|
try {
|
|
1041
|
-
const s = await promises.stat(p);
|
|
1194
|
+
const s = await promises$1.stat(p);
|
|
1042
1195
|
if (s.isFile()) {
|
|
1043
1196
|
finalPath = p;
|
|
1044
1197
|
stats = s;
|
|
@@ -1067,7 +1220,7 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1067
1220
|
for (const idx of indexes) {
|
|
1068
1221
|
const idxPath = path.join(finalPath, idx);
|
|
1069
1222
|
try {
|
|
1070
|
-
const idxStats = await promises.stat(idxPath);
|
|
1223
|
+
const idxStats = await promises$1.stat(idxPath);
|
|
1071
1224
|
if (idxStats.isFile()) {
|
|
1072
1225
|
finalPath = idxPath;
|
|
1073
1226
|
foundIndex = true;
|
|
@@ -1079,7 +1232,7 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1079
1232
|
if (!foundIndex) {
|
|
1080
1233
|
if (config.listDirectory) {
|
|
1081
1234
|
try {
|
|
1082
|
-
const files = await promises.readdir(requestPath);
|
|
1235
|
+
const files = await promises$1.readdir(requestPath);
|
|
1083
1236
|
const listing = eta$1.renderString(`
|
|
1084
1237
|
<!DOCTYPE html>
|
|
1085
1238
|
<html>
|
|
@@ -1116,16 +1269,209 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1116
1269
|
}
|
|
1117
1270
|
}
|
|
1118
1271
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1272
|
+
let response;
|
|
1273
|
+
if (typeof Bun !== "undefined") {
|
|
1274
|
+
response = new Response(Bun.file(finalPath));
|
|
1275
|
+
} else {
|
|
1276
|
+
const fileBuffer = await promises$1.readFile(finalPath);
|
|
1277
|
+
response = new Response(fileBuffer);
|
|
1278
|
+
}
|
|
1121
1279
|
if (config.hooks?.onResponse) {
|
|
1122
1280
|
const hooked = await config.hooks.onResponse(ctx, response);
|
|
1123
1281
|
if (hooked) response = hooked;
|
|
1124
1282
|
}
|
|
1125
1283
|
return response;
|
|
1126
1284
|
};
|
|
1285
|
+
serveStaticMiddleware.isBuiltin = true;
|
|
1286
|
+
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1287
|
+
return serveStaticMiddleware;
|
|
1288
|
+
}
|
|
1289
|
+
class RouterTrie {
|
|
1290
|
+
root;
|
|
1291
|
+
constructor() {
|
|
1292
|
+
this.root = this.createNode();
|
|
1293
|
+
}
|
|
1294
|
+
createNode() {
|
|
1295
|
+
return {
|
|
1296
|
+
children: {}
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
insert(method, path2, handler) {
|
|
1300
|
+
let node2 = this.root;
|
|
1301
|
+
const segments = this.splitPath(path2);
|
|
1302
|
+
for (const segment of segments) {
|
|
1303
|
+
if (segment === "**") {
|
|
1304
|
+
if (!node2.recursiveChild) {
|
|
1305
|
+
node2.recursiveChild = this.createNode();
|
|
1306
|
+
}
|
|
1307
|
+
node2 = node2.recursiveChild;
|
|
1308
|
+
} else if (segment === "*") {
|
|
1309
|
+
if (!node2.wildcardChild) {
|
|
1310
|
+
node2.wildcardChild = this.createNode();
|
|
1311
|
+
}
|
|
1312
|
+
node2 = node2.wildcardChild;
|
|
1313
|
+
} else if (segment.startsWith(":")) {
|
|
1314
|
+
const paramName = segment.slice(1);
|
|
1315
|
+
if (!node2.paramChild) {
|
|
1316
|
+
node2.paramChild = this.createNode();
|
|
1317
|
+
node2.paramChild.paramName = paramName;
|
|
1318
|
+
}
|
|
1319
|
+
node2 = node2.paramChild;
|
|
1320
|
+
node2.paramName = paramName;
|
|
1321
|
+
} else {
|
|
1322
|
+
if (!node2.children[segment]) {
|
|
1323
|
+
node2.children[segment] = this.createNode();
|
|
1324
|
+
}
|
|
1325
|
+
node2 = node2.children[segment];
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (!node2.handlers) {
|
|
1329
|
+
node2.handlers = {};
|
|
1330
|
+
}
|
|
1331
|
+
node2.handlers[method] = handler;
|
|
1332
|
+
}
|
|
1333
|
+
search(method, path2) {
|
|
1334
|
+
const segments = this.splitPath(path2);
|
|
1335
|
+
const params = {};
|
|
1336
|
+
const match = this.findNode(this.root, segments, 0, params);
|
|
1337
|
+
if (match && match.handlers) {
|
|
1338
|
+
const handler = match.handlers[method] || match.handlers["ALL"];
|
|
1339
|
+
if (handler) {
|
|
1340
|
+
return { handler, params };
|
|
1341
|
+
}
|
|
1342
|
+
if (method === "HEAD" && match.handlers["GET"]) {
|
|
1343
|
+
return { handler: match.handlers["GET"], params };
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
findNode(node2, segments, index, params) {
|
|
1349
|
+
if (index === segments.length) {
|
|
1350
|
+
if (node2.handlers) return node2;
|
|
1351
|
+
if (node2.recursiveChild && node2.recursiveChild.handlers) {
|
|
1352
|
+
return node2.recursiveChild;
|
|
1353
|
+
}
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
const segment = segments[index];
|
|
1357
|
+
const child = node2.children[segment];
|
|
1358
|
+
if (child) {
|
|
1359
|
+
const result = this.findNode(child, segments, index + 1, params);
|
|
1360
|
+
if (result) return result;
|
|
1361
|
+
}
|
|
1362
|
+
if (node2.paramChild) {
|
|
1363
|
+
params[node2.paramChild.paramName] = segment;
|
|
1364
|
+
const result = this.findNode(node2.paramChild, segments, index + 1, params);
|
|
1365
|
+
if (result) return result;
|
|
1366
|
+
delete params[node2.paramChild.paramName];
|
|
1367
|
+
}
|
|
1368
|
+
if (node2.wildcardChild) {
|
|
1369
|
+
const result = this.findNode(node2.wildcardChild, segments, index + 1, params);
|
|
1370
|
+
if (result) return result;
|
|
1371
|
+
}
|
|
1372
|
+
if (node2.recursiveChild) {
|
|
1373
|
+
const remaining = segments.length - index;
|
|
1374
|
+
for (let k = 0; k <= remaining; k++) {
|
|
1375
|
+
const result = this.findNode(node2.recursiveChild, segments, index + k, params);
|
|
1376
|
+
if (result) return result;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
splitPath(path2) {
|
|
1382
|
+
if (path2 === "/" || path2 === "") return [];
|
|
1383
|
+
const s = path2.startsWith("/") ? path2.slice(1) : path2;
|
|
1384
|
+
if (s === "") return [];
|
|
1385
|
+
return s.split("/");
|
|
1386
|
+
}
|
|
1127
1387
|
}
|
|
1128
1388
|
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1389
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1390
|
+
const db = new surrealdb.Surreal({
|
|
1391
|
+
engines: node.createNodeEngines()
|
|
1392
|
+
});
|
|
1393
|
+
const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
|
|
1394
|
+
return db.query(`
|
|
1395
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1396
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1397
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1398
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1399
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1400
|
+
`);
|
|
1401
|
+
});
|
|
1402
|
+
const datastore = {
|
|
1403
|
+
get(store, key) {
|
|
1404
|
+
return db.select(new surrealdb.RecordId(store, key));
|
|
1405
|
+
},
|
|
1406
|
+
set(store, key, value) {
|
|
1407
|
+
return db.create(new surrealdb.RecordId(store, key)).content(value);
|
|
1408
|
+
},
|
|
1409
|
+
async query(query, vars) {
|
|
1410
|
+
try {
|
|
1411
|
+
const r = await db.query(query, vars).collect();
|
|
1412
|
+
return r;
|
|
1413
|
+
} catch (e) {
|
|
1414
|
+
console.error("DS ERROR:", e);
|
|
1415
|
+
throw e;
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
ready
|
|
1419
|
+
};
|
|
1420
|
+
process.on("exit", async () => {
|
|
1421
|
+
await db.close();
|
|
1422
|
+
});
|
|
1423
|
+
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1424
|
+
function traceHandler(fn, name) {
|
|
1425
|
+
return async function(...args) {
|
|
1426
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1427
|
+
kind: api.SpanKind.INTERNAL,
|
|
1428
|
+
attributes: {
|
|
1429
|
+
"http.route": name,
|
|
1430
|
+
"component": "shokupan.route"
|
|
1431
|
+
}
|
|
1432
|
+
}, async (span) => {
|
|
1433
|
+
try {
|
|
1434
|
+
const result = await fn.apply(this, args);
|
|
1435
|
+
return result;
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
span.recordException(err);
|
|
1438
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
1439
|
+
throw err;
|
|
1440
|
+
} finally {
|
|
1441
|
+
span.end();
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function getCallerInfo(skipFrames = 1) {
|
|
1447
|
+
let file = "unknown";
|
|
1448
|
+
let line = 0;
|
|
1449
|
+
try {
|
|
1450
|
+
const err = new Error();
|
|
1451
|
+
const stack = err.stack?.split("\n") || [];
|
|
1452
|
+
let found = 0;
|
|
1453
|
+
for (let i = 1; i < stack.length; i++) {
|
|
1454
|
+
const l = stack[i];
|
|
1455
|
+
if (!l.includes(":")) continue;
|
|
1456
|
+
if (l.includes("node_modules")) continue;
|
|
1457
|
+
if (l.includes("bun:main")) continue;
|
|
1458
|
+
if (l.includes("src/util/stack.ts")) continue;
|
|
1459
|
+
if (l.includes("src/router.ts")) continue;
|
|
1460
|
+
if (l.includes("src/shokupan.ts")) continue;
|
|
1461
|
+
found++;
|
|
1462
|
+
if (found >= skipFrames) {
|
|
1463
|
+
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1464
|
+
if (match) {
|
|
1465
|
+
file = match[1];
|
|
1466
|
+
line = parseInt(match[2], 10);
|
|
1467
|
+
return { file, line };
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
} catch (e) {
|
|
1472
|
+
}
|
|
1473
|
+
return { file, line };
|
|
1474
|
+
}
|
|
1129
1475
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1130
1476
|
const ShokupanApplicationTree = {};
|
|
1131
1477
|
class ShokupanRouter {
|
|
@@ -1145,6 +1491,7 @@ class ShokupanRouter {
|
|
|
1145
1491
|
[$parent] = null;
|
|
1146
1492
|
[$childRouters] = [];
|
|
1147
1493
|
[$childControllers] = [];
|
|
1494
|
+
middleware = [];
|
|
1148
1495
|
get rootConfig() {
|
|
1149
1496
|
return this[$appRoot]?.applicationConfig;
|
|
1150
1497
|
}
|
|
@@ -1153,7 +1500,54 @@ class ShokupanRouter {
|
|
|
1153
1500
|
}
|
|
1154
1501
|
[$routes] = [];
|
|
1155
1502
|
// Public via Symbol for OpenAPI generator
|
|
1503
|
+
trie = new RouterTrie();
|
|
1504
|
+
metadata;
|
|
1505
|
+
// Metadata for the router itself
|
|
1156
1506
|
currentGuards = [];
|
|
1507
|
+
// Registry Accessor
|
|
1508
|
+
getComponentRegistry() {
|
|
1509
|
+
const routes = this[$routes].map((r) => ({
|
|
1510
|
+
type: "route",
|
|
1511
|
+
path: r.path,
|
|
1512
|
+
method: r.method,
|
|
1513
|
+
metadata: r.metadata,
|
|
1514
|
+
handlerName: r.handler.name,
|
|
1515
|
+
tags: r.handlerSpec?.tags,
|
|
1516
|
+
order: r.order,
|
|
1517
|
+
_fn: r.handler
|
|
1518
|
+
// Expose handler for debugging instrumentation
|
|
1519
|
+
}));
|
|
1520
|
+
const mw = this.middleware;
|
|
1521
|
+
const middleware = mw ? mw.map((m) => ({
|
|
1522
|
+
name: m.name || "middleware",
|
|
1523
|
+
metadata: m.metadata,
|
|
1524
|
+
order: m.order,
|
|
1525
|
+
_fn: m
|
|
1526
|
+
// Expose function for debugging instrumentation
|
|
1527
|
+
})) : [];
|
|
1528
|
+
const routers = this[$childRouters].map((r) => ({
|
|
1529
|
+
type: "router",
|
|
1530
|
+
path: r[$mountPath],
|
|
1531
|
+
metadata: r.metadata,
|
|
1532
|
+
children: r.getComponentRegistry()
|
|
1533
|
+
}));
|
|
1534
|
+
const controllers = this[$childControllers].map((c) => {
|
|
1535
|
+
return {
|
|
1536
|
+
type: "controller",
|
|
1537
|
+
path: c[$mountPath] || "/",
|
|
1538
|
+
name: c.constructor.name,
|
|
1539
|
+
metadata: c.metadata
|
|
1540
|
+
// Check if we can store this
|
|
1541
|
+
};
|
|
1542
|
+
});
|
|
1543
|
+
return {
|
|
1544
|
+
metadata: this.metadata,
|
|
1545
|
+
middleware,
|
|
1546
|
+
routes,
|
|
1547
|
+
routers,
|
|
1548
|
+
controllers
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1157
1551
|
isRouterInstance(target) {
|
|
1158
1552
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1159
1553
|
}
|
|
@@ -1179,6 +1573,14 @@ class ShokupanRouter {
|
|
|
1179
1573
|
throw new Error("Router is already mounted");
|
|
1180
1574
|
}
|
|
1181
1575
|
controller[$mountPath] = prefix;
|
|
1576
|
+
if (!controller.metadata) {
|
|
1577
|
+
const info = getCallerInfo();
|
|
1578
|
+
controller.metadata = {
|
|
1579
|
+
file: info.file,
|
|
1580
|
+
line: info.line,
|
|
1581
|
+
name: "MountedRouter"
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1182
1584
|
this[$childRouters].push(controller);
|
|
1183
1585
|
controller[$parent] = this;
|
|
1184
1586
|
const setRouterContext = (router) => {
|
|
@@ -1211,6 +1613,12 @@ class ShokupanRouter {
|
|
|
1211
1613
|
}
|
|
1212
1614
|
}
|
|
1213
1615
|
instance[$mountPath] = prefix;
|
|
1616
|
+
const info = getCallerInfo();
|
|
1617
|
+
instance.metadata = {
|
|
1618
|
+
file: info.file,
|
|
1619
|
+
line: info.line,
|
|
1620
|
+
name: instance.constructor.name
|
|
1621
|
+
};
|
|
1214
1622
|
this[$childControllers].push(instance);
|
|
1215
1623
|
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1216
1624
|
const proto = Object.getPrototypeOf(instance);
|
|
@@ -1294,14 +1702,39 @@ class ShokupanRouter {
|
|
|
1294
1702
|
for (const arg of sortedArgs) {
|
|
1295
1703
|
switch (arg.type) {
|
|
1296
1704
|
case RouteParamType.BODY:
|
|
1297
|
-
|
|
1705
|
+
try {
|
|
1706
|
+
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
1707
|
+
args[arg.index] = await ctx.req.json();
|
|
1708
|
+
} else {
|
|
1709
|
+
const text = await ctx.req.text();
|
|
1710
|
+
if (!text) {
|
|
1711
|
+
args[arg.index] = {};
|
|
1712
|
+
} else {
|
|
1713
|
+
args[arg.index] = JSON.parse(text);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
} catch (e) {
|
|
1717
|
+
const err = new Error("Invalid JSON body");
|
|
1718
|
+
err.status = 400;
|
|
1719
|
+
throw err;
|
|
1720
|
+
}
|
|
1298
1721
|
break;
|
|
1299
1722
|
case RouteParamType.PARAM:
|
|
1300
1723
|
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
1301
1724
|
break;
|
|
1302
1725
|
case RouteParamType.QUERY: {
|
|
1303
1726
|
const url = new URL(ctx.req.url);
|
|
1304
|
-
|
|
1727
|
+
if (arg.name) {
|
|
1728
|
+
const vals = url.searchParams.getAll(arg.name);
|
|
1729
|
+
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1730
|
+
} else {
|
|
1731
|
+
const query = {};
|
|
1732
|
+
for (const key of url.searchParams.keys()) {
|
|
1733
|
+
const vals = url.searchParams.getAll(key);
|
|
1734
|
+
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1735
|
+
}
|
|
1736
|
+
args[arg.index] = query;
|
|
1737
|
+
}
|
|
1305
1738
|
break;
|
|
1306
1739
|
}
|
|
1307
1740
|
case RouteParamType.HEADER:
|
|
@@ -1316,7 +1749,7 @@ class ShokupanRouter {
|
|
|
1316
1749
|
}
|
|
1317
1750
|
}
|
|
1318
1751
|
}
|
|
1319
|
-
const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
|
|
1752
|
+
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
1320
1753
|
return tracedOriginalHandler.apply(instance, args);
|
|
1321
1754
|
};
|
|
1322
1755
|
let finalHandler = wrappedHandler;
|
|
@@ -1449,29 +1882,59 @@ class ShokupanRouter {
|
|
|
1449
1882
|
data: result
|
|
1450
1883
|
};
|
|
1451
1884
|
}
|
|
1452
|
-
|
|
1885
|
+
applyRouterHooks(match) {
|
|
1453
1886
|
if (!this.config?.hooks) return match;
|
|
1454
1887
|
const hooks = this.config.hooks;
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1888
|
+
return {
|
|
1889
|
+
...match,
|
|
1890
|
+
handler: this.wrapWithHooks(match.handler, hooks)
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
wrapWithHooks(handler, hooks) {
|
|
1894
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1895
|
+
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1896
|
+
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1897
|
+
const hasError = hookList.some((h) => !!h.onError);
|
|
1898
|
+
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1899
|
+
const originalHandler = handler;
|
|
1900
|
+
const wrapped = async (ctx) => {
|
|
1901
|
+
if (hasStart) {
|
|
1902
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1903
|
+
const h = hookList[i];
|
|
1904
|
+
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
const debug = ctx._debug;
|
|
1908
|
+
let debugId;
|
|
1909
|
+
let previousNode;
|
|
1910
|
+
if (debug) {
|
|
1911
|
+
debugId = originalHandler._debugId || originalHandler.name || "handler";
|
|
1912
|
+
previousNode = debug.getCurrentNode();
|
|
1913
|
+
debug.trackEdge(previousNode, debugId);
|
|
1914
|
+
debug.setNode(debugId);
|
|
1915
|
+
}
|
|
1916
|
+
const start = performance.now();
|
|
1917
|
+
try {
|
|
1918
|
+
const res = await originalHandler(ctx);
|
|
1919
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1920
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1921
|
+
const h = hookList[i];
|
|
1922
|
+
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1923
|
+
}
|
|
1924
|
+
return res;
|
|
1462
1925
|
} catch (err) {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
console.error("Error in router onError hook:", e);
|
|
1468
|
-
}
|
|
1926
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1927
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1928
|
+
const h = hookList[i];
|
|
1929
|
+
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1469
1930
|
}
|
|
1470
1931
|
throw err;
|
|
1932
|
+
} finally {
|
|
1933
|
+
if (debug && previousNode) debug.setNode(previousNode);
|
|
1471
1934
|
}
|
|
1472
1935
|
};
|
|
1473
|
-
|
|
1474
|
-
return
|
|
1936
|
+
wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
|
|
1937
|
+
return wrapped;
|
|
1475
1938
|
}
|
|
1476
1939
|
/**
|
|
1477
1940
|
* Find a route matching the given method and path.
|
|
@@ -1480,29 +1943,24 @@ class ShokupanRouter {
|
|
|
1480
1943
|
* @returns Route handler and parameters if found, otherwise null
|
|
1481
1944
|
*/
|
|
1482
1945
|
find(method, path2) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
route.keys.forEach((key, index) => {
|
|
1489
|
-
params[key] = match[index + 1];
|
|
1490
|
-
});
|
|
1491
|
-
return this.applyHooks({ handler: route.handler, params });
|
|
1492
|
-
}
|
|
1946
|
+
let result = this.trie.search(method, path2);
|
|
1947
|
+
if (result) return result;
|
|
1948
|
+
if (method === "HEAD") {
|
|
1949
|
+
result = this.trie.search("GET", path2);
|
|
1950
|
+
if (result) return result;
|
|
1493
1951
|
}
|
|
1494
1952
|
for (const child of this[$childRouters]) {
|
|
1495
1953
|
const prefix = child[$mountPath];
|
|
1496
1954
|
if (path2 === prefix || path2.startsWith(prefix + "/")) {
|
|
1497
1955
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1498
1956
|
const match = child.find(method, subPath);
|
|
1499
|
-
if (match) return this.
|
|
1957
|
+
if (match) return this.applyRouterHooks(match);
|
|
1500
1958
|
}
|
|
1501
1959
|
if (prefix.endsWith("/")) {
|
|
1502
1960
|
if (path2.startsWith(prefix)) {
|
|
1503
1961
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1504
1962
|
const match = child.find(method, subPath);
|
|
1505
|
-
if (match) return this.
|
|
1963
|
+
if (match) return this.applyRouterHooks(match);
|
|
1506
1964
|
}
|
|
1507
1965
|
}
|
|
1508
1966
|
}
|
|
@@ -1513,7 +1971,7 @@ class ShokupanRouter {
|
|
|
1513
1971
|
const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1514
1972
|
keys.push(key);
|
|
1515
1973
|
return "([^/]+)";
|
|
1516
|
-
}).replace(
|
|
1974
|
+
}).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
|
|
1517
1975
|
return {
|
|
1518
1976
|
regex: new RegExp(`^${pattern}$`),
|
|
1519
1977
|
keys
|
|
@@ -1582,18 +2040,84 @@ class ShokupanRouter {
|
|
|
1582
2040
|
return innerHandler(ctx);
|
|
1583
2041
|
};
|
|
1584
2042
|
}
|
|
2043
|
+
const { file, line } = getCallerInfo();
|
|
2044
|
+
const trackingHandler = wrappedHandler;
|
|
2045
|
+
wrappedHandler = async (ctx) => {
|
|
2046
|
+
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2047
|
+
return trackingHandler(ctx);
|
|
2048
|
+
}
|
|
2049
|
+
const startTime = performance.now();
|
|
2050
|
+
let error = void 0;
|
|
2051
|
+
try {
|
|
2052
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2053
|
+
ctx.handlerStack.push({
|
|
2054
|
+
name: handler.name || "anonymous",
|
|
2055
|
+
file,
|
|
2056
|
+
line
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
return await trackingHandler(ctx);
|
|
2060
|
+
} catch (e) {
|
|
2061
|
+
error = e;
|
|
2062
|
+
throw e;
|
|
2063
|
+
} finally {
|
|
2064
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2065
|
+
const duration = performance.now() - startTime;
|
|
2066
|
+
const config = ctx.app.applicationConfig;
|
|
2067
|
+
try {
|
|
2068
|
+
const timestamp = Date.now();
|
|
2069
|
+
const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
|
|
2070
|
+
await datastore.set("middleware_tracking", key, {
|
|
2071
|
+
name: handler.name || "anonymous",
|
|
2072
|
+
path: ctx.path,
|
|
2073
|
+
timestamp,
|
|
2074
|
+
duration,
|
|
2075
|
+
file,
|
|
2076
|
+
line,
|
|
2077
|
+
error: error ? String(error) : void 0,
|
|
2078
|
+
metadata: {
|
|
2079
|
+
isBuiltin: handler.isBuiltin,
|
|
2080
|
+
pluginName: handler.pluginName
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2084
|
+
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2085
|
+
const cutoff = Date.now() - ttl;
|
|
2086
|
+
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2087
|
+
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2088
|
+
if (results && results[0] && results[0].count > maxCapacity) {
|
|
2089
|
+
const toDelete = results[0].count - maxCapacity;
|
|
2090
|
+
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2091
|
+
}
|
|
2092
|
+
} catch (datastoreError) {
|
|
2093
|
+
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2099
|
+
let bakedHandler = wrappedHandler;
|
|
2100
|
+
if (this.config?.hooks) {
|
|
2101
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
|
|
2102
|
+
}
|
|
1585
2103
|
this[$routes].push({
|
|
1586
2104
|
method,
|
|
1587
2105
|
path: path2,
|
|
1588
|
-
regex,
|
|
1589
|
-
keys,
|
|
1590
|
-
handler
|
|
2106
|
+
regex: regex ?? new RegExp(""),
|
|
2107
|
+
keys: keys ?? [],
|
|
2108
|
+
handler,
|
|
2109
|
+
bakedHandler,
|
|
1591
2110
|
handlerSpec: spec,
|
|
1592
2111
|
group,
|
|
1593
|
-
|
|
1594
|
-
requestTimeout
|
|
1595
|
-
|
|
2112
|
+
hooks: this.config?.hooks,
|
|
2113
|
+
requestTimeout,
|
|
2114
|
+
renderer,
|
|
2115
|
+
metadata: {
|
|
2116
|
+
file,
|
|
2117
|
+
line
|
|
2118
|
+
}
|
|
1596
2119
|
});
|
|
2120
|
+
this.trie.insert(method, path2, bakedHandler);
|
|
1597
2121
|
return this;
|
|
1598
2122
|
}
|
|
1599
2123
|
get(path2, ...args) {
|
|
@@ -1627,7 +2151,35 @@ class ShokupanRouter {
|
|
|
1627
2151
|
guard(specOrHandler, handler) {
|
|
1628
2152
|
const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
|
|
1629
2153
|
const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
|
|
1630
|
-
|
|
2154
|
+
let file = "unknown";
|
|
2155
|
+
let line = 0;
|
|
2156
|
+
try {
|
|
2157
|
+
const err = new Error();
|
|
2158
|
+
const stack = err.stack?.split("\n") || [];
|
|
2159
|
+
const callerLine = stack.find(
|
|
2160
|
+
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
2161
|
+
);
|
|
2162
|
+
if (callerLine) {
|
|
2163
|
+
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
2164
|
+
if (match) {
|
|
2165
|
+
file = match[1];
|
|
2166
|
+
line = parseInt(match[2], 10);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
} catch (e) {
|
|
2170
|
+
}
|
|
2171
|
+
const trackedGuard = async (ctx, next) => {
|
|
2172
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2173
|
+
ctx.handlerStack.push({
|
|
2174
|
+
name: guardHandler.name || "guard",
|
|
2175
|
+
file,
|
|
2176
|
+
line
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
return guardHandler(ctx, next);
|
|
2180
|
+
};
|
|
2181
|
+
trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
|
|
2182
|
+
this.currentGuards.push({ handler: trackedGuard, spec });
|
|
1631
2183
|
return this;
|
|
1632
2184
|
}
|
|
1633
2185
|
/**
|
|
@@ -1639,10 +2191,10 @@ class ShokupanRouter {
|
|
|
1639
2191
|
const config = typeof options === "string" ? { root: options } : options;
|
|
1640
2192
|
const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
|
|
1641
2193
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1642
|
-
serveStatic(
|
|
2194
|
+
const handlerMiddleware = serveStatic(config, prefix);
|
|
1643
2195
|
const routeHandler = async (ctx) => {
|
|
1644
|
-
|
|
1645
|
-
|
|
2196
|
+
return handlerMiddleware(ctx, async () => {
|
|
2197
|
+
});
|
|
1646
2198
|
};
|
|
1647
2199
|
let groupName = "Static";
|
|
1648
2200
|
const segments = normalizedPrefix.split("/").filter(Boolean);
|
|
@@ -1703,6 +2255,49 @@ class ShokupanRouter {
|
|
|
1703
2255
|
return generateOpenApi(this, options);
|
|
1704
2256
|
}
|
|
1705
2257
|
}
|
|
2258
|
+
class SystemCpuMonitor {
|
|
2259
|
+
constructor(intervalMs = 1e3) {
|
|
2260
|
+
this.intervalMs = intervalMs;
|
|
2261
|
+
}
|
|
2262
|
+
interval = null;
|
|
2263
|
+
lastCpus = [];
|
|
2264
|
+
currentUsage = 0;
|
|
2265
|
+
start() {
|
|
2266
|
+
if (this.interval) return;
|
|
2267
|
+
this.lastCpus = os__namespace.cpus();
|
|
2268
|
+
this.interval = setInterval(() => this.update(), this.intervalMs);
|
|
2269
|
+
}
|
|
2270
|
+
stop() {
|
|
2271
|
+
if (this.interval) {
|
|
2272
|
+
clearInterval(this.interval);
|
|
2273
|
+
this.interval = null;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
getUsage() {
|
|
2277
|
+
return this.currentUsage;
|
|
2278
|
+
}
|
|
2279
|
+
update() {
|
|
2280
|
+
const cpus = os__namespace.cpus();
|
|
2281
|
+
let idle = 0;
|
|
2282
|
+
let total = 0;
|
|
2283
|
+
for (let i = 0; i < cpus.length; i++) {
|
|
2284
|
+
const cpu = cpus[i];
|
|
2285
|
+
const prev = this.lastCpus[i];
|
|
2286
|
+
let type;
|
|
2287
|
+
for (type in cpu.times) {
|
|
2288
|
+
const ticks = cpu.times[type];
|
|
2289
|
+
const prevTicks = prev.times[type];
|
|
2290
|
+
const diff = ticks - prevTicks;
|
|
2291
|
+
total += diff;
|
|
2292
|
+
if (type === "idle") {
|
|
2293
|
+
idle += diff;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
this.lastCpus = cpus;
|
|
2298
|
+
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
1706
2301
|
const defaults = {
|
|
1707
2302
|
port: 3e3,
|
|
1708
2303
|
hostname: "localhost",
|
|
@@ -1714,21 +2309,59 @@ api.trace.getTracer("shokupan.application");
|
|
|
1714
2309
|
class Shokupan extends ShokupanRouter {
|
|
1715
2310
|
applicationConfig = {};
|
|
1716
2311
|
openApiSpec;
|
|
1717
|
-
|
|
2312
|
+
composedMiddleware;
|
|
2313
|
+
cpuMonitor;
|
|
2314
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
2315
|
+
hooksInitialized = false;
|
|
1718
2316
|
get logger() {
|
|
1719
2317
|
return this.applicationConfig.logger;
|
|
1720
2318
|
}
|
|
1721
2319
|
constructor(applicationConfig = {}) {
|
|
1722
|
-
|
|
2320
|
+
const config = Object.assign({}, defaults, applicationConfig);
|
|
2321
|
+
const { hooks, ...routerConfig } = config;
|
|
2322
|
+
super(routerConfig);
|
|
1723
2323
|
this[$isApplication] = true;
|
|
1724
2324
|
this[$appRoot] = this;
|
|
1725
|
-
|
|
2325
|
+
this.applicationConfig = config;
|
|
2326
|
+
const { file, line } = getCallerInfo();
|
|
2327
|
+
this.metadata = {
|
|
2328
|
+
file,
|
|
2329
|
+
line,
|
|
2330
|
+
name: "ShokupanApplication"
|
|
2331
|
+
};
|
|
1726
2332
|
}
|
|
1727
2333
|
/**
|
|
1728
2334
|
* Adds middleware to the application.
|
|
1729
2335
|
*/
|
|
1730
2336
|
use(middleware) {
|
|
1731
|
-
|
|
2337
|
+
let trackedMiddleware = middleware;
|
|
2338
|
+
const { file, line } = getCallerInfo();
|
|
2339
|
+
if (!middleware.metadata) {
|
|
2340
|
+
middleware.metadata = {
|
|
2341
|
+
file,
|
|
2342
|
+
line,
|
|
2343
|
+
name: middleware.name || "middleware",
|
|
2344
|
+
isBuiltin: middleware.isBuiltin,
|
|
2345
|
+
pluginName: middleware.pluginName
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
trackedMiddleware = async (ctx, next) => {
|
|
2349
|
+
const c = ctx;
|
|
2350
|
+
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2351
|
+
const metadata = middleware.metadata || {};
|
|
2352
|
+
c.handlerStack.push({
|
|
2353
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2354
|
+
file: metadata.file || file,
|
|
2355
|
+
line: metadata.line || line,
|
|
2356
|
+
isBuiltin: metadata.isBuiltin
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
return middleware(ctx, next);
|
|
2360
|
+
};
|
|
2361
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2362
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2363
|
+
trackedMiddleware.order = this.middleware.length;
|
|
2364
|
+
this.middleware.push(trackedMiddleware);
|
|
1732
2365
|
return this;
|
|
1733
2366
|
}
|
|
1734
2367
|
startupHooks = [];
|
|
@@ -1739,6 +2372,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1739
2372
|
this.startupHooks.push(callback);
|
|
1740
2373
|
return this;
|
|
1741
2374
|
}
|
|
2375
|
+
specAvailableHooks = [];
|
|
2376
|
+
/**
|
|
2377
|
+
* Registers a callback to be executed when the OpenAPI spec is available.
|
|
2378
|
+
* This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
|
|
2379
|
+
*/
|
|
2380
|
+
onSpecAvailable(callback) {
|
|
2381
|
+
this.specAvailableHooks.push(callback);
|
|
2382
|
+
return this;
|
|
2383
|
+
}
|
|
1742
2384
|
/**
|
|
1743
2385
|
* Starts the application server.
|
|
1744
2386
|
*
|
|
@@ -1755,17 +2397,43 @@ class Shokupan extends ShokupanRouter {
|
|
|
1755
2397
|
}
|
|
1756
2398
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
1757
2399
|
this.openApiSpec = await generateOpenApi(this);
|
|
2400
|
+
for (const hook of this.specAvailableHooks) {
|
|
2401
|
+
await hook(this.openApiSpec);
|
|
2402
|
+
}
|
|
1758
2403
|
}
|
|
1759
2404
|
if (port === 0 && process.platform === "linux") ;
|
|
2405
|
+
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2406
|
+
this.cpuMonitor = new SystemCpuMonitor();
|
|
2407
|
+
this.cpuMonitor.start();
|
|
2408
|
+
}
|
|
1760
2409
|
const serveOptions = {
|
|
1761
2410
|
port: finalPort,
|
|
1762
2411
|
hostname: this.applicationConfig.hostname,
|
|
1763
2412
|
development: this.applicationConfig.development,
|
|
1764
2413
|
fetch: this.fetch.bind(this),
|
|
1765
2414
|
reusePort: this.applicationConfig.reusePort,
|
|
1766
|
-
idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0
|
|
2415
|
+
idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
|
|
2416
|
+
websocket: {
|
|
2417
|
+
open(ws) {
|
|
2418
|
+
ws.data?.handler?.open?.(ws);
|
|
2419
|
+
},
|
|
2420
|
+
message(ws, message) {
|
|
2421
|
+
ws.data?.handler?.message?.(ws, message);
|
|
2422
|
+
},
|
|
2423
|
+
drain(ws) {
|
|
2424
|
+
ws.data?.handler?.drain?.(ws);
|
|
2425
|
+
},
|
|
2426
|
+
close(ws, code, reason) {
|
|
2427
|
+
ws.data?.handler?.close?.(ws, code, reason);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
1767
2430
|
};
|
|
1768
|
-
|
|
2431
|
+
let factory = this.applicationConfig.serverFactory;
|
|
2432
|
+
if (!factory && typeof Bun === "undefined") {
|
|
2433
|
+
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-fVKP60e0.cjs"));
|
|
2434
|
+
factory = createHttpServer();
|
|
2435
|
+
}
|
|
2436
|
+
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
1769
2437
|
console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
|
|
1770
2438
|
return server;
|
|
1771
2439
|
}
|
|
@@ -1820,110 +2488,165 @@ class Shokupan extends ShokupanRouter {
|
|
|
1820
2488
|
* @returns The response to send.
|
|
1821
2489
|
*/
|
|
1822
2490
|
async fetch(req, server) {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2491
|
+
if (this.applicationConfig.enableTracing) {
|
|
2492
|
+
const tracer2 = api.trace.getTracer("shokupan.application");
|
|
2493
|
+
const store = asyncContext.getStore();
|
|
2494
|
+
const attrs = {
|
|
2495
|
+
attributes: {
|
|
2496
|
+
"http.url": req.url,
|
|
2497
|
+
"http.method": req.method
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
const parent = store?.get("span");
|
|
2501
|
+
const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
|
|
2502
|
+
return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
|
|
2503
|
+
const ctxMap = /* @__PURE__ */ new Map();
|
|
2504
|
+
ctxMap.set("span", span);
|
|
2505
|
+
ctxMap.set("request", req);
|
|
2506
|
+
return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
if (this.applicationConfig.enableAsyncLocalStorage) {
|
|
1834
2510
|
const ctxMap = /* @__PURE__ */ new Map();
|
|
1835
|
-
ctxMap.set("span", span);
|
|
1836
2511
|
ctxMap.set("request", req);
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
if (result instanceof Response) {
|
|
1856
|
-
response = result;
|
|
1857
|
-
} else if (result === null || result === void 0) {
|
|
1858
|
-
span.setAttribute("http.status_code", 404);
|
|
1859
|
-
response = ctx2.text("Not Found", 404);
|
|
1860
|
-
} else if (typeof result === "object") {
|
|
1861
|
-
response = ctx2.json(result);
|
|
1862
|
-
} else {
|
|
1863
|
-
response = ctx2.text(String(result));
|
|
1864
|
-
}
|
|
1865
|
-
if (this.applicationConfig.hooks?.onRequestEnd) {
|
|
1866
|
-
await this.applicationConfig.hooks.onRequestEnd(ctx2);
|
|
1867
|
-
}
|
|
1868
|
-
if (this.applicationConfig.hooks?.onResponseStart) {
|
|
1869
|
-
await this.applicationConfig.hooks.onResponseStart(ctx2, response);
|
|
1870
|
-
}
|
|
1871
|
-
return response;
|
|
1872
|
-
} catch (err) {
|
|
1873
|
-
console.error(err);
|
|
1874
|
-
span.recordException(err);
|
|
1875
|
-
span.setStatus({ code: 2 });
|
|
1876
|
-
const status = err.status || err.statusCode || 500;
|
|
1877
|
-
const body = { error: err.message || "Internal Server Error" };
|
|
1878
|
-
if (err.errors) body.errors = err.errors;
|
|
1879
|
-
if (this.applicationConfig.hooks?.onError) {
|
|
1880
|
-
try {
|
|
1881
|
-
await this.applicationConfig.hooks.onError(err, ctx2);
|
|
1882
|
-
} catch (hookErr) {
|
|
1883
|
-
console.error("Error in onError hook:", hookErr);
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
return ctx2.json(body, status);
|
|
1887
|
-
}
|
|
1888
|
-
};
|
|
1889
|
-
let executionPromise = handle();
|
|
1890
|
-
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
1891
|
-
if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
|
|
1892
|
-
let timeoutId;
|
|
1893
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
1894
|
-
timeoutId = setTimeout(async () => {
|
|
1895
|
-
try {
|
|
1896
|
-
if (this.applicationConfig.hooks?.onRequestTimeout) {
|
|
1897
|
-
await this.applicationConfig.hooks.onRequestTimeout(ctx2);
|
|
1898
|
-
}
|
|
1899
|
-
} catch (e) {
|
|
1900
|
-
console.error("Error in onRequestTimeout hook:", e);
|
|
1901
|
-
}
|
|
1902
|
-
reject(new Error("Request Timeout"));
|
|
1903
|
-
}, timeoutMs);
|
|
1904
|
-
});
|
|
1905
|
-
executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
2512
|
+
return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
|
|
2513
|
+
}
|
|
2514
|
+
return this.handleRequest(req, server);
|
|
2515
|
+
}
|
|
2516
|
+
async handleRequest(req, server) {
|
|
2517
|
+
const request = req;
|
|
2518
|
+
const controller = new AbortController();
|
|
2519
|
+
const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
|
|
2520
|
+
const handle = async () => {
|
|
2521
|
+
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2522
|
+
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2523
|
+
const res = ctx.text(msg, 429);
|
|
2524
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2525
|
+
return res;
|
|
2526
|
+
}
|
|
2527
|
+
try {
|
|
2528
|
+
if (this.hasHook("onRequestStart")) {
|
|
2529
|
+
await this.executeHook("onRequestStart", ctx);
|
|
1906
2530
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2531
|
+
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2532
|
+
const result = await fn(ctx, async () => {
|
|
2533
|
+
const match = this.find(req.method, ctx.path);
|
|
2534
|
+
if (match) {
|
|
2535
|
+
ctx.params = match.params;
|
|
2536
|
+
return match.handler(ctx);
|
|
1910
2537
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2538
|
+
return null;
|
|
2539
|
+
});
|
|
2540
|
+
let response;
|
|
2541
|
+
if (result instanceof Response) {
|
|
2542
|
+
response = result;
|
|
2543
|
+
} else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
|
|
2544
|
+
response = ctx._finalResponse;
|
|
2545
|
+
} else if (result === null || result === void 0) {
|
|
2546
|
+
if (ctx._finalResponse instanceof Response) {
|
|
2547
|
+
response = ctx._finalResponse;
|
|
2548
|
+
} else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
|
|
2549
|
+
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2550
|
+
} else {
|
|
2551
|
+
response = ctx.text("Not Found", 404);
|
|
1916
2552
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2553
|
+
} else if (typeof result === "object") {
|
|
2554
|
+
response = ctx.json(result);
|
|
2555
|
+
} else {
|
|
2556
|
+
response = ctx.text(String(result));
|
|
2557
|
+
}
|
|
2558
|
+
if (this.hasHook("onRequestEnd")) {
|
|
2559
|
+
await this.executeHook("onRequestEnd", ctx);
|
|
2560
|
+
}
|
|
2561
|
+
if (this.hasHook("onResponseStart")) {
|
|
2562
|
+
await this.executeHook("onResponseStart", ctx, response);
|
|
2563
|
+
}
|
|
2564
|
+
return response;
|
|
2565
|
+
} catch (err) {
|
|
2566
|
+
console.error(err);
|
|
2567
|
+
const span = asyncContext.getStore()?.get("span");
|
|
2568
|
+
if (span) span.setStatus({ code: 2 });
|
|
2569
|
+
const status = err.status || err.statusCode || 500;
|
|
2570
|
+
const body = { error: err.message || "Internal Server Error" };
|
|
2571
|
+
if (err.errors) body.errors = err.errors;
|
|
2572
|
+
if (this.hasHook("onError")) {
|
|
2573
|
+
await this.executeHook("onError", err, ctx);
|
|
2574
|
+
}
|
|
2575
|
+
return ctx.json(body, status);
|
|
1924
2576
|
}
|
|
2577
|
+
};
|
|
2578
|
+
let executionPromise = handle();
|
|
2579
|
+
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
2580
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2581
|
+
let timeoutId;
|
|
2582
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2583
|
+
timeoutId = setTimeout(async () => {
|
|
2584
|
+
controller.abort();
|
|
2585
|
+
if (this.hasHook("onRequestTimeout")) {
|
|
2586
|
+
await this.executeHook("onRequestTimeout", ctx);
|
|
2587
|
+
}
|
|
2588
|
+
reject(new Error("Request Timeout"));
|
|
2589
|
+
}, timeoutMs);
|
|
2590
|
+
});
|
|
2591
|
+
executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
2592
|
+
}
|
|
2593
|
+
return executionPromise.catch((err) => {
|
|
2594
|
+
if (err.message === "Request Timeout") {
|
|
2595
|
+
return ctx.text("Request Timeout", 408);
|
|
2596
|
+
}
|
|
2597
|
+
console.error("Unexpected error in request execution:", err);
|
|
2598
|
+
return ctx.text("Internal Server Error", 500);
|
|
2599
|
+
}).then(async (res) => {
|
|
2600
|
+
if (this.hasHook("onResponseEnd")) {
|
|
2601
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2602
|
+
}
|
|
2603
|
+
return res;
|
|
1925
2604
|
});
|
|
1926
2605
|
}
|
|
2606
|
+
ensureHooksInitialized() {
|
|
2607
|
+
const hooks = this.applicationConfig.hooks;
|
|
2608
|
+
if (hooks) {
|
|
2609
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2610
|
+
const hookTypes = [
|
|
2611
|
+
"onRequestStart",
|
|
2612
|
+
"onRequestEnd",
|
|
2613
|
+
"onResponseStart",
|
|
2614
|
+
"onResponseEnd",
|
|
2615
|
+
"onError",
|
|
2616
|
+
"beforeValidate",
|
|
2617
|
+
"afterValidate",
|
|
2618
|
+
"onRequestTimeout",
|
|
2619
|
+
"onReadTimeout",
|
|
2620
|
+
"onWriteTimeout"
|
|
2621
|
+
];
|
|
2622
|
+
for (const type of hookTypes) {
|
|
2623
|
+
const fns = [];
|
|
2624
|
+
for (const h of hookList) {
|
|
2625
|
+
if (h[type]) fns.push(h[type]);
|
|
2626
|
+
}
|
|
2627
|
+
if (fns.length > 0) {
|
|
2628
|
+
this.hookCache.set(type, fns);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
this.hooksInitialized = true;
|
|
2633
|
+
}
|
|
2634
|
+
async executeHook(name, ...args) {
|
|
2635
|
+
if (!this.hooksInitialized) {
|
|
2636
|
+
this.ensureHooksInitialized();
|
|
2637
|
+
}
|
|
2638
|
+
const fns = this.hookCache.get(name);
|
|
2639
|
+
if (!fns) return;
|
|
2640
|
+
for (const fn of fns) {
|
|
2641
|
+
await fn(...args);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
hasHook(name) {
|
|
2645
|
+
if (!this.hooksInitialized) {
|
|
2646
|
+
this.ensureHooksInitialized();
|
|
2647
|
+
}
|
|
2648
|
+
return this.hookCache.has(name);
|
|
2649
|
+
}
|
|
1927
2650
|
}
|
|
1928
2651
|
class AuthPlugin extends ShokupanRouter {
|
|
1929
2652
|
constructor(authConfig) {
|
|
@@ -2128,7 +2851,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2128
2851
|
/**
|
|
2129
2852
|
* Middleware to verify JWT
|
|
2130
2853
|
*/
|
|
2131
|
-
|
|
2854
|
+
getMiddleware() {
|
|
2132
2855
|
return async (ctx, next) => {
|
|
2133
2856
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
2134
2857
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
@@ -2148,19 +2871,44 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2148
2871
|
}
|
|
2149
2872
|
}
|
|
2150
2873
|
function Compression(options = {}) {
|
|
2151
|
-
const threshold = options.threshold ??
|
|
2152
|
-
|
|
2874
|
+
const threshold = options.threshold ?? 512;
|
|
2875
|
+
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
2153
2876
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
2154
2877
|
let method = null;
|
|
2155
2878
|
if (acceptEncoding.includes("br")) method = "br";
|
|
2156
|
-
else if (acceptEncoding.includes("
|
|
2879
|
+
else if (acceptEncoding.includes("zstd")) {
|
|
2880
|
+
if (typeof Bun === "undefined") {
|
|
2881
|
+
throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
|
|
2882
|
+
}
|
|
2883
|
+
method = "zstd";
|
|
2884
|
+
} else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
2157
2885
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
2158
2886
|
if (!method) return next();
|
|
2159
|
-
|
|
2887
|
+
let response = await next();
|
|
2888
|
+
if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
|
|
2889
|
+
response = ctx._finalResponse;
|
|
2890
|
+
}
|
|
2160
2891
|
if (response instanceof Response) {
|
|
2161
2892
|
if (response.headers.has("Content-Encoding")) return response;
|
|
2162
|
-
|
|
2163
|
-
|
|
2893
|
+
let body;
|
|
2894
|
+
let bodySize;
|
|
2895
|
+
if (ctx._rawBody !== void 0) {
|
|
2896
|
+
if (typeof ctx._rawBody === "string") {
|
|
2897
|
+
const encoded = new TextEncoder().encode(ctx._rawBody);
|
|
2898
|
+
body = encoded.buffer;
|
|
2899
|
+
bodySize = encoded.byteLength;
|
|
2900
|
+
} else if (ctx._rawBody instanceof Uint8Array) {
|
|
2901
|
+
body = ctx._rawBody.buffer;
|
|
2902
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2903
|
+
} else {
|
|
2904
|
+
body = ctx._rawBody;
|
|
2905
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2906
|
+
}
|
|
2907
|
+
} else {
|
|
2908
|
+
body = await response.arrayBuffer();
|
|
2909
|
+
bodySize = body.byteLength;
|
|
2910
|
+
}
|
|
2911
|
+
if (bodySize < threshold) {
|
|
2164
2912
|
return new Response(body, {
|
|
2165
2913
|
status: response.status,
|
|
2166
2914
|
statusText: response.statusText,
|
|
@@ -2168,17 +2916,36 @@ function Compression(options = {}) {
|
|
|
2168
2916
|
});
|
|
2169
2917
|
}
|
|
2170
2918
|
let compressed;
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2919
|
+
switch (method) {
|
|
2920
|
+
case "br":
|
|
2921
|
+
compressed = await new Promise((res, rej) => zlib__namespace.brotliCompress(body, {
|
|
2922
|
+
params: {
|
|
2923
|
+
[zlib__namespace.constants.BROTLI_PARAM_QUALITY]: 4
|
|
2924
|
+
}
|
|
2925
|
+
}, (err, data) => {
|
|
2926
|
+
if (err) return rej(err);
|
|
2927
|
+
res(data);
|
|
2928
|
+
}));
|
|
2929
|
+
break;
|
|
2930
|
+
case "gzip":
|
|
2931
|
+
compressed = await new Promise((res, rej) => zlib__namespace.gzip(body, (err, data) => {
|
|
2932
|
+
if (err) return rej(err);
|
|
2933
|
+
res(data);
|
|
2934
|
+
}));
|
|
2935
|
+
break;
|
|
2936
|
+
case "zstd":
|
|
2937
|
+
compressed = await Bun.zstdCompress(body);
|
|
2938
|
+
break;
|
|
2939
|
+
default:
|
|
2940
|
+
compressed = await new Promise((res, rej) => zlib__namespace.deflate(body, (err, data) => {
|
|
2941
|
+
if (err) return rej(err);
|
|
2942
|
+
res(data);
|
|
2943
|
+
}));
|
|
2944
|
+
break;
|
|
2177
2945
|
}
|
|
2178
2946
|
const headers = new Headers(response.headers);
|
|
2179
2947
|
headers.set("Content-Encoding", method);
|
|
2180
2948
|
headers.set("Content-Length", String(compressed.length));
|
|
2181
|
-
headers.delete("Content-Length");
|
|
2182
2949
|
return new Response(compressed, {
|
|
2183
2950
|
status: response.status,
|
|
2184
2951
|
statusText: response.statusText,
|
|
@@ -2187,6 +2954,9 @@ function Compression(options = {}) {
|
|
|
2187
2954
|
}
|
|
2188
2955
|
return response;
|
|
2189
2956
|
};
|
|
2957
|
+
compressionMiddleware.isBuiltin = true;
|
|
2958
|
+
compressionMiddleware.pluginName = "Compression";
|
|
2959
|
+
return compressionMiddleware;
|
|
2190
2960
|
}
|
|
2191
2961
|
function Cors(options = {}) {
|
|
2192
2962
|
const defaults2 = {
|
|
@@ -2196,7 +2966,7 @@ function Cors(options = {}) {
|
|
|
2196
2966
|
optionsSuccessStatus: 204
|
|
2197
2967
|
};
|
|
2198
2968
|
const opts = { ...defaults2, ...options };
|
|
2199
|
-
|
|
2969
|
+
const corsMiddleware = async function CorsMiddleware(ctx, next) {
|
|
2200
2970
|
const headers = new Headers();
|
|
2201
2971
|
const origin = ctx.headers.get("origin");
|
|
2202
2972
|
const set = (k, v) => headers.set(k, v);
|
|
@@ -2258,6 +3028,9 @@ function Cors(options = {}) {
|
|
|
2258
3028
|
}
|
|
2259
3029
|
return response;
|
|
2260
3030
|
};
|
|
3031
|
+
corsMiddleware.isBuiltin = true;
|
|
3032
|
+
corsMiddleware.pluginName = "Cors";
|
|
3033
|
+
return corsMiddleware;
|
|
2261
3034
|
}
|
|
2262
3035
|
function useExpress(expressMiddleware) {
|
|
2263
3036
|
return async (ctx, next) => {
|
|
@@ -2319,122 +3092,409 @@ function useExpress(expressMiddleware) {
|
|
|
2319
3092
|
});
|
|
2320
3093
|
};
|
|
2321
3094
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
2329
|
-
return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
|
|
2330
|
-
});
|
|
2331
|
-
const skip = options.skip || (() => false);
|
|
2332
|
-
const hits = /* @__PURE__ */ new Map();
|
|
2333
|
-
const interval = setInterval(() => {
|
|
2334
|
-
const now = Date.now();
|
|
2335
|
-
for (const [key, record] of hits.entries()) {
|
|
2336
|
-
if (record.resetTime <= now) {
|
|
2337
|
-
hits.delete(key);
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
}, windowMs);
|
|
2341
|
-
if (interval.unref) interval.unref();
|
|
2342
|
-
return async (ctx, next) => {
|
|
2343
|
-
if (skip(ctx)) return next();
|
|
2344
|
-
const key = keyGenerator(ctx);
|
|
2345
|
-
const now = Date.now();
|
|
2346
|
-
let record = hits.get(key);
|
|
2347
|
-
if (!record || record.resetTime <= now) {
|
|
2348
|
-
record = {
|
|
2349
|
-
hits: 0,
|
|
2350
|
-
resetTime: now + windowMs
|
|
2351
|
-
};
|
|
2352
|
-
hits.set(key, record);
|
|
2353
|
-
}
|
|
2354
|
-
record.hits++;
|
|
2355
|
-
const remaining = Math.max(0, max - record.hits);
|
|
2356
|
-
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
2357
|
-
if (record.hits > max) {
|
|
2358
|
-
if (headers) {
|
|
2359
|
-
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2360
|
-
res.headers.set("X-RateLimit-Limit", String(max));
|
|
2361
|
-
res.headers.set("X-RateLimit-Remaining", "0");
|
|
2362
|
-
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2363
|
-
return res;
|
|
2364
|
-
}
|
|
2365
|
-
return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2366
|
-
}
|
|
2367
|
-
const response = await next();
|
|
2368
|
-
if (response instanceof Response && headers) {
|
|
2369
|
-
response.headers.set("X-RateLimit-Limit", String(max));
|
|
2370
|
-
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
2371
|
-
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2372
|
-
}
|
|
2373
|
-
return response;
|
|
2374
|
-
};
|
|
3095
|
+
class ValidationError extends Error {
|
|
3096
|
+
constructor(errors) {
|
|
3097
|
+
super("Validation Error");
|
|
3098
|
+
this.errors = errors;
|
|
3099
|
+
}
|
|
3100
|
+
status = 400;
|
|
2375
3101
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
3102
|
+
function isZod(schema) {
|
|
3103
|
+
return typeof schema?.safeParse === "function";
|
|
3104
|
+
}
|
|
3105
|
+
async function validateZod(schema, data) {
|
|
3106
|
+
const result = await schema.safeParseAsync(data);
|
|
3107
|
+
if (!result.success) {
|
|
3108
|
+
throw new ValidationError(result.error.errors);
|
|
2382
3109
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
<meta charset = "utf-8" />
|
|
2392
|
-
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
2393
|
-
</head>
|
|
2394
|
-
|
|
2395
|
-
<body>
|
|
2396
|
-
<div id="app"></div>
|
|
2397
|
-
|
|
2398
|
-
<script src="<%= it.path %>scalar.js"><\/script>
|
|
2399
|
-
<script>
|
|
2400
|
-
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
2401
|
-
url: "<%= it.path %>openapi.json",
|
|
2402
|
-
}
|
|
2403
|
-
])
|
|
2404
|
-
<\/script>
|
|
2405
|
-
</body>
|
|
2406
|
-
|
|
2407
|
-
</html>`, { path: path2, config: this.pluginOptions }));
|
|
2408
|
-
});
|
|
2409
|
-
this.get("/scalar.js", (ctx) => {
|
|
2410
|
-
return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
|
|
2411
|
-
});
|
|
2412
|
-
this.get("/openapi.json", async (ctx) => {
|
|
2413
|
-
let spec;
|
|
2414
|
-
if (this.root.openApiSpec) {
|
|
2415
|
-
try {
|
|
2416
|
-
spec = structuredClone(this.root.openApiSpec);
|
|
2417
|
-
} catch (e) {
|
|
2418
|
-
spec = Object.assign({}, this.root.openApiSpec);
|
|
2419
|
-
}
|
|
2420
|
-
} else {
|
|
2421
|
-
spec = await (this.root || this).generateApiSpec();
|
|
2422
|
-
}
|
|
2423
|
-
if (this.pluginOptions.baseDocument) {
|
|
2424
|
-
deepMerge(spec, this.pluginOptions.baseDocument);
|
|
2425
|
-
}
|
|
2426
|
-
return ctx.json(spec);
|
|
2427
|
-
});
|
|
3110
|
+
return result.data;
|
|
3111
|
+
}
|
|
3112
|
+
function isTypeBox(schema) {
|
|
3113
|
+
return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
|
|
3114
|
+
}
|
|
3115
|
+
function validateTypeBox(schema, data) {
|
|
3116
|
+
if (!schema.Check(data)) {
|
|
3117
|
+
throw new ValidationError([...schema.Errors(data)]);
|
|
2428
3118
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
3119
|
+
return data;
|
|
3120
|
+
}
|
|
3121
|
+
function isAjv(schema) {
|
|
3122
|
+
return typeof schema === "function" && "errors" in schema;
|
|
3123
|
+
}
|
|
3124
|
+
function validateAjv(schema, data) {
|
|
3125
|
+
const valid = schema(data);
|
|
3126
|
+
if (!valid) {
|
|
3127
|
+
throw new ValidationError(schema.errors);
|
|
3128
|
+
}
|
|
3129
|
+
return data;
|
|
3130
|
+
}
|
|
3131
|
+
const valibot = (schema, parser) => {
|
|
3132
|
+
return {
|
|
3133
|
+
_valibot: true,
|
|
3134
|
+
schema,
|
|
3135
|
+
parser
|
|
3136
|
+
};
|
|
3137
|
+
};
|
|
3138
|
+
function isValibotWrapper(schema) {
|
|
3139
|
+
return schema?._valibot === true;
|
|
3140
|
+
}
|
|
3141
|
+
async function validateValibotWrapper(wrapper, data) {
|
|
3142
|
+
const result = await wrapper.parser(wrapper.schema, data);
|
|
3143
|
+
if (!result.success) {
|
|
3144
|
+
throw new ValidationError(result.issues);
|
|
3145
|
+
}
|
|
3146
|
+
return result.output;
|
|
3147
|
+
}
|
|
3148
|
+
function isClass(schema) {
|
|
3149
|
+
try {
|
|
3150
|
+
if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
|
|
3151
|
+
return true;
|
|
3152
|
+
}
|
|
3153
|
+
return typeof schema === "function" && schema.prototype && schema.name;
|
|
3154
|
+
} catch {
|
|
3155
|
+
return false;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
async function validateClassValidator(schema, data) {
|
|
3159
|
+
const object = classTransformer.plainToInstance(schema, data);
|
|
3160
|
+
try {
|
|
3161
|
+
await classValidator.validateOrReject(object);
|
|
3162
|
+
return object;
|
|
3163
|
+
} catch (errors) {
|
|
3164
|
+
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
3165
|
+
property: err.property,
|
|
3166
|
+
constraints: err.constraints,
|
|
3167
|
+
children: err.children
|
|
3168
|
+
})) : errors;
|
|
3169
|
+
throw new ValidationError(formattedErrors);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
const safelyGetBody = async (ctx) => {
|
|
3173
|
+
const req = ctx.req;
|
|
3174
|
+
if (req._bodyParsed) {
|
|
3175
|
+
return req._bodyValue;
|
|
3176
|
+
}
|
|
3177
|
+
try {
|
|
3178
|
+
let data;
|
|
3179
|
+
if (typeof req.json === "function") {
|
|
3180
|
+
data = await req.json();
|
|
3181
|
+
} else {
|
|
3182
|
+
data = req.body;
|
|
3183
|
+
if (typeof data === "string") {
|
|
3184
|
+
try {
|
|
3185
|
+
data = JSON.parse(data);
|
|
3186
|
+
} catch {
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
req._bodyParsed = true;
|
|
3191
|
+
req._bodyValue = data;
|
|
3192
|
+
Object.defineProperty(req, "json", {
|
|
3193
|
+
value: async () => req._bodyValue,
|
|
3194
|
+
configurable: true
|
|
3195
|
+
});
|
|
3196
|
+
return data;
|
|
3197
|
+
} catch (e) {
|
|
3198
|
+
return {};
|
|
3199
|
+
}
|
|
3200
|
+
};
|
|
3201
|
+
function getValidator(schema) {
|
|
3202
|
+
if (isZod(schema)) {
|
|
3203
|
+
return (data) => validateZod(schema, data);
|
|
3204
|
+
}
|
|
3205
|
+
if (isTypeBox(schema)) {
|
|
3206
|
+
return (data) => validateTypeBox(schema, data);
|
|
3207
|
+
}
|
|
3208
|
+
if (isAjv(schema)) {
|
|
3209
|
+
return (data) => validateAjv(schema, data);
|
|
3210
|
+
}
|
|
3211
|
+
if (isValibotWrapper(schema)) {
|
|
3212
|
+
return (data) => validateValibotWrapper(schema, data);
|
|
3213
|
+
}
|
|
3214
|
+
if (isClass(schema)) {
|
|
3215
|
+
return (data) => validateClassValidator(schema, data);
|
|
3216
|
+
}
|
|
3217
|
+
if (typeof schema === "function") {
|
|
3218
|
+
return schema;
|
|
3219
|
+
}
|
|
3220
|
+
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
3221
|
+
}
|
|
3222
|
+
function validate(config) {
|
|
3223
|
+
const validators = {};
|
|
3224
|
+
if (config.params) validators.params = getValidator(config.params);
|
|
3225
|
+
if (config.query) validators.query = getValidator(config.query);
|
|
3226
|
+
if (config.headers) validators.headers = getValidator(config.headers);
|
|
3227
|
+
if (config.body) validators.body = getValidator(config.body);
|
|
3228
|
+
return async (ctx, next) => {
|
|
3229
|
+
const dataToValidate = {};
|
|
3230
|
+
if (config.params) dataToValidate.params = ctx.params;
|
|
3231
|
+
let queryObj;
|
|
3232
|
+
if (config.query) {
|
|
3233
|
+
const url = new URL(ctx.req.url);
|
|
3234
|
+
queryObj = Object.fromEntries(url.searchParams.entries());
|
|
3235
|
+
dataToValidate.query = queryObj;
|
|
3236
|
+
}
|
|
3237
|
+
if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
|
|
3238
|
+
let body;
|
|
3239
|
+
if (config.body) {
|
|
3240
|
+
body = await safelyGetBody(ctx);
|
|
3241
|
+
dataToValidate.body = body;
|
|
3242
|
+
}
|
|
3243
|
+
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
3244
|
+
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
3245
|
+
}
|
|
3246
|
+
if (validators.params) {
|
|
3247
|
+
ctx.params = await validators.params(ctx.params);
|
|
3248
|
+
}
|
|
3249
|
+
let validQuery;
|
|
3250
|
+
if (validators.query && queryObj) {
|
|
3251
|
+
validQuery = await validators.query(queryObj);
|
|
3252
|
+
}
|
|
3253
|
+
if (validators.headers) {
|
|
3254
|
+
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
3255
|
+
await validators.headers(headersObj);
|
|
3256
|
+
}
|
|
3257
|
+
let validBody;
|
|
3258
|
+
if (validators.body) {
|
|
3259
|
+
const b = body ?? await safelyGetBody(ctx);
|
|
3260
|
+
validBody = await validators.body(b);
|
|
3261
|
+
const req = ctx.req;
|
|
3262
|
+
req._bodyValue = validBody;
|
|
3263
|
+
Object.defineProperty(req, "json", {
|
|
3264
|
+
value: async () => validBody,
|
|
3265
|
+
configurable: true
|
|
3266
|
+
});
|
|
3267
|
+
ctx.body = validBody;
|
|
3268
|
+
}
|
|
3269
|
+
if (ctx.app?.applicationConfig.hooks?.afterValidate) {
|
|
3270
|
+
const validatedData = { ...dataToValidate };
|
|
3271
|
+
if (config.params) validatedData.params = ctx.params;
|
|
3272
|
+
if (config.query) validatedData.query = validQuery;
|
|
3273
|
+
if (config.body) validatedData.body = validBody;
|
|
3274
|
+
await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
|
|
3275
|
+
}
|
|
3276
|
+
return next();
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
const ajv = new Ajv({ coerceTypes: true, allErrors: true });
|
|
3280
|
+
addFormats(ajv);
|
|
3281
|
+
const compiledValidators = /* @__PURE__ */ new WeakMap();
|
|
3282
|
+
function openApiValidator() {
|
|
3283
|
+
return async (ctx, next) => {
|
|
3284
|
+
const app = ctx.app;
|
|
3285
|
+
if (!app || !app.openApiSpec) {
|
|
3286
|
+
return next();
|
|
3287
|
+
}
|
|
3288
|
+
let cache = compiledValidators.get(app);
|
|
3289
|
+
if (!cache) {
|
|
3290
|
+
cache = compileValidators(app.openApiSpec);
|
|
3291
|
+
compiledValidators.set(app, cache);
|
|
3292
|
+
}
|
|
3293
|
+
let matchPath;
|
|
3294
|
+
let matchParams = {};
|
|
3295
|
+
if (cache.validators.has(ctx.path)) {
|
|
3296
|
+
matchPath = ctx.path;
|
|
3297
|
+
} else {
|
|
3298
|
+
for (const [path2, { regex, paramNames }] of cache.paths) {
|
|
3299
|
+
const match = regex.exec(ctx.path);
|
|
3300
|
+
if (match) {
|
|
3301
|
+
matchPath = path2;
|
|
3302
|
+
paramNames.forEach((name, i) => {
|
|
3303
|
+
matchParams[name] = match[i + 1];
|
|
3304
|
+
});
|
|
3305
|
+
break;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
if (!matchPath) {
|
|
3310
|
+
return next();
|
|
3311
|
+
}
|
|
3312
|
+
const method = ctx.req.method.toLowerCase();
|
|
3313
|
+
const validators = cache.validators.get(matchPath)?.[method];
|
|
3314
|
+
if (!validators) {
|
|
3315
|
+
return next();
|
|
3316
|
+
}
|
|
3317
|
+
const errors = [];
|
|
3318
|
+
if (validators.body) {
|
|
3319
|
+
let body;
|
|
3320
|
+
try {
|
|
3321
|
+
body = await ctx.req.json().catch(() => ({}));
|
|
3322
|
+
} catch {
|
|
3323
|
+
body = {};
|
|
3324
|
+
}
|
|
3325
|
+
const valid = validators.body(body);
|
|
3326
|
+
if (!valid && validators.body.errors) {
|
|
3327
|
+
errors.push(...validators.body.errors.map((e) => ({ ...e, location: "body" })));
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
if (validators.query) {
|
|
3331
|
+
const query = Object.fromEntries(new URL(ctx.req.url).searchParams.entries());
|
|
3332
|
+
const valid = validators.query(query);
|
|
3333
|
+
if (!valid && validators.query.errors) {
|
|
3334
|
+
errors.push(...validators.query.errors.map((e) => ({ ...e, location: "query" })));
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
if (validators.params) {
|
|
3338
|
+
const params = { ...matchParams, ...ctx.params };
|
|
3339
|
+
const valid = validators.params(params);
|
|
3340
|
+
if (!valid && validators.params.errors) {
|
|
3341
|
+
errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
if (validators.headers) {
|
|
3345
|
+
const headers = Object.fromEntries(ctx.req.headers.entries());
|
|
3346
|
+
const valid = validators.headers(headers);
|
|
3347
|
+
if (!valid && validators.headers.errors) {
|
|
3348
|
+
errors.push(...validators.headers.errors.map((e) => ({ ...e, location: "header" })));
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
if (errors.length > 0) {
|
|
3352
|
+
throw new ValidationError(errors);
|
|
3353
|
+
}
|
|
3354
|
+
return next();
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
function compileValidators(spec) {
|
|
3358
|
+
const validators = /* @__PURE__ */ new Map();
|
|
3359
|
+
const paths = /* @__PURE__ */ new Map();
|
|
3360
|
+
for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
|
|
3361
|
+
if (path2.includes("{")) {
|
|
3362
|
+
const paramNames = [];
|
|
3363
|
+
const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
|
|
3364
|
+
paramNames.push(name);
|
|
3365
|
+
return "([^/]+)";
|
|
3366
|
+
}) + "$";
|
|
3367
|
+
paths.set(path2, {
|
|
3368
|
+
regex: new RegExp(regexStr),
|
|
3369
|
+
paramNames
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
const pathValidators = {};
|
|
3373
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
3374
|
+
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
3375
|
+
const oper = operation;
|
|
3376
|
+
const opValidators = {};
|
|
3377
|
+
if (oper.requestBody?.content?.["application/json"]?.schema) {
|
|
3378
|
+
opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
|
|
3379
|
+
}
|
|
3380
|
+
const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
|
|
3381
|
+
const queryProps = {};
|
|
3382
|
+
const pathProps = {};
|
|
3383
|
+
const headerProps = {};
|
|
3384
|
+
const queryRequired = [];
|
|
3385
|
+
const pathRequired = [];
|
|
3386
|
+
const headerRequired = [];
|
|
3387
|
+
for (const param of parameters) {
|
|
3388
|
+
if (param.in === "query") {
|
|
3389
|
+
queryProps[param.name] = param.schema || {};
|
|
3390
|
+
if (param.required) queryRequired.push(param.name);
|
|
3391
|
+
} else if (param.in === "path") {
|
|
3392
|
+
pathProps[param.name] = param.schema || {};
|
|
3393
|
+
pathRequired.push(param.name);
|
|
3394
|
+
} else if (param.in === "header") {
|
|
3395
|
+
headerProps[param.name] = param.schema || {};
|
|
3396
|
+
if (param.required) headerRequired.push(param.name);
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
if (Object.keys(queryProps).length > 0) {
|
|
3400
|
+
opValidators.query = ajv.compile({
|
|
3401
|
+
type: "object",
|
|
3402
|
+
properties: queryProps,
|
|
3403
|
+
required: queryRequired.length > 0 ? queryRequired : void 0
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
if (Object.keys(pathProps).length > 0) {
|
|
3407
|
+
opValidators.params = ajv.compile({
|
|
3408
|
+
type: "object",
|
|
3409
|
+
properties: pathProps,
|
|
3410
|
+
required: pathRequired.length > 0 ? pathRequired : void 0
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
if (Object.keys(headerProps).length > 0) {
|
|
3414
|
+
opValidators.headers = ajv.compile({
|
|
3415
|
+
type: "object",
|
|
3416
|
+
properties: headerProps,
|
|
3417
|
+
required: headerRequired.length > 0 ? headerRequired : void 0
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
pathValidators[method] = opValidators;
|
|
3421
|
+
}
|
|
3422
|
+
validators.set(path2, pathValidators);
|
|
3423
|
+
}
|
|
3424
|
+
return { paths, validators };
|
|
3425
|
+
}
|
|
3426
|
+
function precompileValidators(app, spec) {
|
|
3427
|
+
const cache = compileValidators(spec);
|
|
3428
|
+
compiledValidators.set(app, cache);
|
|
3429
|
+
}
|
|
3430
|
+
function enableOpenApiValidation(app) {
|
|
3431
|
+
app.use(openApiValidator());
|
|
3432
|
+
app.onSpecAvailable((spec) => {
|
|
3433
|
+
precompileValidators(app, spec);
|
|
3434
|
+
});
|
|
3435
|
+
}
|
|
3436
|
+
const eta = new eta$2.Eta();
|
|
3437
|
+
class ScalarPlugin extends ShokupanRouter {
|
|
3438
|
+
constructor(pluginOptions) {
|
|
3439
|
+
super();
|
|
3440
|
+
this.pluginOptions = pluginOptions;
|
|
3441
|
+
this.init();
|
|
3442
|
+
}
|
|
3443
|
+
init() {
|
|
3444
|
+
this.get("/", (ctx) => {
|
|
3445
|
+
let path2 = ctx.url.toString();
|
|
3446
|
+
if (!path2.endsWith("/")) path2 += "/";
|
|
3447
|
+
return ctx.html(eta.renderString(`<!doctype html>
|
|
3448
|
+
<html>
|
|
3449
|
+
<head>
|
|
3450
|
+
<title>API Reference</title>
|
|
3451
|
+
<meta charset = "utf-8" />
|
|
3452
|
+
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
3453
|
+
</head>
|
|
3454
|
+
|
|
3455
|
+
<body>
|
|
3456
|
+
<div id="app"></div>
|
|
3457
|
+
|
|
3458
|
+
<script src="<%= it.path %>scalar.js"><\/script>
|
|
3459
|
+
<script>
|
|
3460
|
+
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
3461
|
+
url: "<%= it.path %>openapi.json",
|
|
3462
|
+
}
|
|
3463
|
+
])
|
|
3464
|
+
<\/script>
|
|
3465
|
+
</body>
|
|
3466
|
+
|
|
3467
|
+
</html>`, { path: path2, config: this.pluginOptions }));
|
|
3468
|
+
});
|
|
3469
|
+
this.get("/scalar.js", (ctx) => {
|
|
3470
|
+
return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
|
|
3471
|
+
});
|
|
3472
|
+
this.get("/openapi.json", async (ctx) => {
|
|
3473
|
+
let spec;
|
|
3474
|
+
if (this.root.openApiSpec) {
|
|
3475
|
+
try {
|
|
3476
|
+
spec = structuredClone(this.root.openApiSpec);
|
|
3477
|
+
} catch (e) {
|
|
3478
|
+
spec = Object.assign({}, this.root.openApiSpec);
|
|
3479
|
+
}
|
|
3480
|
+
} else {
|
|
3481
|
+
spec = await (this.root || this).generateApiSpec();
|
|
3482
|
+
}
|
|
3483
|
+
if (this.pluginOptions.baseDocument) {
|
|
3484
|
+
deepMerge(spec, this.pluginOptions.baseDocument);
|
|
3485
|
+
}
|
|
3486
|
+
return ctx.json(spec);
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
// New lifecycle method to be called by router.mount
|
|
3490
|
+
onMount(parent) {
|
|
3491
|
+
if (parent.onStart) {
|
|
3492
|
+
parent.onStart(async () => {
|
|
3493
|
+
if (this.pluginOptions.enableStaticAnalysis) {
|
|
3494
|
+
try {
|
|
3495
|
+
const entrypoint = process.argv[1];
|
|
3496
|
+
console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
|
|
3497
|
+
const analyzer = new openapiAnalyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
2438
3498
|
let staticSpec = await analyzer.analyze();
|
|
2439
3499
|
if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
|
|
2440
3500
|
deepMerge(this.pluginOptions.baseDocument, staticSpec);
|
|
@@ -2448,7 +3508,7 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
2448
3508
|
}
|
|
2449
3509
|
}
|
|
2450
3510
|
function SecurityHeaders(options = {}) {
|
|
2451
|
-
|
|
3511
|
+
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
2452
3512
|
const headers = {};
|
|
2453
3513
|
const set = (k, v) => headers[k] = v;
|
|
2454
3514
|
if (options.dnsPrefetchControl !== false) {
|
|
@@ -2502,6 +3562,9 @@ function SecurityHeaders(options = {}) {
|
|
|
2502
3562
|
}
|
|
2503
3563
|
return response;
|
|
2504
3564
|
};
|
|
3565
|
+
securityHeadersMiddleware.isBuiltin = true;
|
|
3566
|
+
securityHeadersMiddleware.pluginName = "SecurityHeaders";
|
|
3567
|
+
return securityHeadersMiddleware;
|
|
2505
3568
|
}
|
|
2506
3569
|
class Cookie {
|
|
2507
3570
|
maxAge;
|
|
@@ -2615,7 +3678,7 @@ function Session(options) {
|
|
|
2615
3678
|
const resave = options.resave === void 0 ? true : options.resave;
|
|
2616
3679
|
const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
|
|
2617
3680
|
const rolling = options.rolling || false;
|
|
2618
|
-
|
|
3681
|
+
const sessionMiddleware = async function SessionMiddleware(ctx, next) {
|
|
2619
3682
|
let reqSessionId = null;
|
|
2620
3683
|
const cookieHeader = ctx.req.headers.get("cookie");
|
|
2621
3684
|
const cookies = {};
|
|
@@ -2751,194 +3814,9 @@ function Session(options) {
|
|
|
2751
3814
|
}
|
|
2752
3815
|
return result;
|
|
2753
3816
|
};
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
super("Validation Error");
|
|
2758
|
-
this.errors = errors;
|
|
2759
|
-
}
|
|
2760
|
-
status = 400;
|
|
2761
|
-
}
|
|
2762
|
-
function isZod(schema) {
|
|
2763
|
-
return typeof schema?.safeParse === "function";
|
|
2764
|
-
}
|
|
2765
|
-
async function validateZod(schema, data) {
|
|
2766
|
-
const result = await schema.safeParseAsync(data);
|
|
2767
|
-
if (!result.success) {
|
|
2768
|
-
throw new ValidationError(result.error.errors);
|
|
2769
|
-
}
|
|
2770
|
-
return result.data;
|
|
2771
|
-
}
|
|
2772
|
-
function isTypeBox(schema) {
|
|
2773
|
-
return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
|
|
2774
|
-
}
|
|
2775
|
-
function validateTypeBox(schema, data) {
|
|
2776
|
-
if (!schema.Check(data)) {
|
|
2777
|
-
throw new ValidationError([...schema.Errors(data)]);
|
|
2778
|
-
}
|
|
2779
|
-
return data;
|
|
2780
|
-
}
|
|
2781
|
-
function isAjv(schema) {
|
|
2782
|
-
return typeof schema === "function" && "errors" in schema;
|
|
2783
|
-
}
|
|
2784
|
-
function validateAjv(schema, data) {
|
|
2785
|
-
const valid = schema(data);
|
|
2786
|
-
if (!valid) {
|
|
2787
|
-
throw new ValidationError(schema.errors);
|
|
2788
|
-
}
|
|
2789
|
-
return data;
|
|
2790
|
-
}
|
|
2791
|
-
const valibot = (schema, parser) => {
|
|
2792
|
-
return {
|
|
2793
|
-
_valibot: true,
|
|
2794
|
-
schema,
|
|
2795
|
-
parser
|
|
2796
|
-
};
|
|
2797
|
-
};
|
|
2798
|
-
function isValibotWrapper(schema) {
|
|
2799
|
-
return schema?._valibot === true;
|
|
2800
|
-
}
|
|
2801
|
-
async function validateValibotWrapper(wrapper, data) {
|
|
2802
|
-
const result = await wrapper.parser(wrapper.schema, data);
|
|
2803
|
-
if (!result.success) {
|
|
2804
|
-
throw new ValidationError(result.issues);
|
|
2805
|
-
}
|
|
2806
|
-
return result.output;
|
|
2807
|
-
}
|
|
2808
|
-
function isClass(schema) {
|
|
2809
|
-
try {
|
|
2810
|
-
if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
|
|
2811
|
-
return true;
|
|
2812
|
-
}
|
|
2813
|
-
return typeof schema === "function" && schema.prototype && schema.name;
|
|
2814
|
-
} catch {
|
|
2815
|
-
return false;
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2818
|
-
async function validateClassValidator(schema, data) {
|
|
2819
|
-
const object = classTransformer.plainToInstance(schema, data);
|
|
2820
|
-
try {
|
|
2821
|
-
await classValidator.validateOrReject(object);
|
|
2822
|
-
return object;
|
|
2823
|
-
} catch (errors) {
|
|
2824
|
-
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
2825
|
-
property: err.property,
|
|
2826
|
-
constraints: err.constraints,
|
|
2827
|
-
children: err.children
|
|
2828
|
-
})) : errors;
|
|
2829
|
-
throw new ValidationError(formattedErrors);
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
const safelyGetBody = async (ctx) => {
|
|
2833
|
-
const req = ctx.req;
|
|
2834
|
-
if (req._bodyParsed) {
|
|
2835
|
-
return req._bodyValue;
|
|
2836
|
-
}
|
|
2837
|
-
try {
|
|
2838
|
-
let data;
|
|
2839
|
-
if (typeof req.json === "function") {
|
|
2840
|
-
data = await req.json();
|
|
2841
|
-
} else {
|
|
2842
|
-
data = req.body;
|
|
2843
|
-
if (typeof data === "string") {
|
|
2844
|
-
try {
|
|
2845
|
-
data = JSON.parse(data);
|
|
2846
|
-
} catch {
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
req._bodyParsed = true;
|
|
2851
|
-
req._bodyValue = data;
|
|
2852
|
-
Object.defineProperty(req, "json", {
|
|
2853
|
-
value: async () => req._bodyValue,
|
|
2854
|
-
configurable: true
|
|
2855
|
-
});
|
|
2856
|
-
return data;
|
|
2857
|
-
} catch (e) {
|
|
2858
|
-
return {};
|
|
2859
|
-
}
|
|
2860
|
-
};
|
|
2861
|
-
function validate(config) {
|
|
2862
|
-
return async (ctx, next) => {
|
|
2863
|
-
const dataToValidate = {};
|
|
2864
|
-
if (config.params) dataToValidate.params = ctx.params;
|
|
2865
|
-
let queryObj;
|
|
2866
|
-
if (config.query) {
|
|
2867
|
-
const url = new URL(ctx.req.url);
|
|
2868
|
-
queryObj = Object.fromEntries(url.searchParams.entries());
|
|
2869
|
-
dataToValidate.query = queryObj;
|
|
2870
|
-
}
|
|
2871
|
-
if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
|
|
2872
|
-
let body;
|
|
2873
|
-
if (config.body) {
|
|
2874
|
-
body = await safelyGetBody(ctx);
|
|
2875
|
-
dataToValidate.body = body;
|
|
2876
|
-
}
|
|
2877
|
-
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
2878
|
-
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
2879
|
-
}
|
|
2880
|
-
if (config.params) {
|
|
2881
|
-
ctx.params = await runValidation(config.params, ctx.params);
|
|
2882
|
-
}
|
|
2883
|
-
let validQuery;
|
|
2884
|
-
if (config.query && queryObj) {
|
|
2885
|
-
validQuery = await runValidation(config.query, queryObj);
|
|
2886
|
-
}
|
|
2887
|
-
if (config.headers) {
|
|
2888
|
-
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2889
|
-
await runValidation(config.headers, headersObj);
|
|
2890
|
-
}
|
|
2891
|
-
let validBody;
|
|
2892
|
-
if (config.body) {
|
|
2893
|
-
const b = body ?? await safelyGetBody(ctx);
|
|
2894
|
-
validBody = await runValidation(config.body, b);
|
|
2895
|
-
const req = ctx.req;
|
|
2896
|
-
req._bodyValue = validBody;
|
|
2897
|
-
Object.defineProperty(req, "json", {
|
|
2898
|
-
value: async () => validBody,
|
|
2899
|
-
configurable: true
|
|
2900
|
-
});
|
|
2901
|
-
ctx.body = validBody;
|
|
2902
|
-
}
|
|
2903
|
-
if (ctx.app?.applicationConfig.hooks?.afterValidate) {
|
|
2904
|
-
const validatedData = { ...dataToValidate };
|
|
2905
|
-
if (config.params) validatedData.params = ctx.params;
|
|
2906
|
-
if (config.query) validatedData.query = validQuery;
|
|
2907
|
-
if (config.body) validatedData.body = validBody;
|
|
2908
|
-
await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
|
|
2909
|
-
}
|
|
2910
|
-
return next();
|
|
2911
|
-
};
|
|
2912
|
-
}
|
|
2913
|
-
async function runValidation(schema, data) {
|
|
2914
|
-
if (isZod(schema)) {
|
|
2915
|
-
return validateZod(schema, data);
|
|
2916
|
-
}
|
|
2917
|
-
if (isTypeBox(schema)) {
|
|
2918
|
-
return validateTypeBox(schema, data);
|
|
2919
|
-
}
|
|
2920
|
-
if (isAjv(schema)) {
|
|
2921
|
-
return validateAjv(schema, data);
|
|
2922
|
-
}
|
|
2923
|
-
if (isValibotWrapper(schema)) {
|
|
2924
|
-
return validateValibotWrapper(schema, data);
|
|
2925
|
-
}
|
|
2926
|
-
if (isClass(schema)) {
|
|
2927
|
-
return validateClassValidator(schema, data);
|
|
2928
|
-
}
|
|
2929
|
-
if (isTypeBox(schema)) {
|
|
2930
|
-
return validateTypeBox(schema, data);
|
|
2931
|
-
}
|
|
2932
|
-
if (isAjv(schema)) {
|
|
2933
|
-
return validateAjv(schema, data);
|
|
2934
|
-
}
|
|
2935
|
-
if (isValibotWrapper(schema)) {
|
|
2936
|
-
return validateValibotWrapper(schema, data);
|
|
2937
|
-
}
|
|
2938
|
-
if (typeof schema === "function") {
|
|
2939
|
-
return schema(data);
|
|
2940
|
-
}
|
|
2941
|
-
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
3817
|
+
sessionMiddleware.isBuiltin = true;
|
|
3818
|
+
sessionMiddleware.pluginName = "Session";
|
|
3819
|
+
return sessionMiddleware;
|
|
2942
3820
|
}
|
|
2943
3821
|
exports.$appRoot = $appRoot;
|
|
2944
3822
|
exports.$childControllers = $childControllers;
|
|
@@ -2978,6 +3856,7 @@ exports.Post = Post;
|
|
|
2978
3856
|
exports.Put = Put;
|
|
2979
3857
|
exports.Query = Query;
|
|
2980
3858
|
exports.RateLimit = RateLimit;
|
|
3859
|
+
exports.RateLimitMiddleware = RateLimitMiddleware;
|
|
2981
3860
|
exports.Req = Req;
|
|
2982
3861
|
exports.RouteParamType = RouteParamType;
|
|
2983
3862
|
exports.RouterRegistry = RouterRegistry;
|
|
@@ -2993,7 +3872,11 @@ exports.ShokupanRouter = ShokupanRouter;
|
|
|
2993
3872
|
exports.Spec = Spec;
|
|
2994
3873
|
exports.Use = Use;
|
|
2995
3874
|
exports.ValidationError = ValidationError;
|
|
3875
|
+
exports.compileValidators = compileValidators;
|
|
2996
3876
|
exports.compose = compose;
|
|
3877
|
+
exports.enableOpenApiValidation = enableOpenApiValidation;
|
|
3878
|
+
exports.openApiValidator = openApiValidator;
|
|
3879
|
+
exports.precompileValidators = precompileValidators;
|
|
2997
3880
|
exports.useExpress = useExpress;
|
|
2998
3881
|
exports.valibot = valibot;
|
|
2999
3882
|
exports.validate = validate;
|