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.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { Eta } from "eta";
|
|
3
|
-
import { stat, readdir } from "fs/promises";
|
|
3
|
+
import { stat, readdir, readFile as readFile$1 } from "fs/promises";
|
|
4
4
|
import { resolve, join, basename } from "path";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import { createNodeEngines } from "@surrealdb/node";
|
|
7
|
+
import { Surreal, RecordId } from "surrealdb";
|
|
8
|
+
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
9
|
+
import * as os from "node:os";
|
|
6
10
|
import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
|
|
7
11
|
import * as jose from "jose";
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
12
|
+
import * as zlib from "node:zlib";
|
|
13
|
+
import Ajv from "ajv";
|
|
14
|
+
import addFormats from "ajv-formats";
|
|
11
15
|
import { plainToInstance } from "class-transformer";
|
|
12
16
|
import { validateOrReject } from "class-validator";
|
|
17
|
+
import { OpenAPIAnalyzer } from "./openapi-analyzer-BtIaHIfe.js";
|
|
18
|
+
import { randomUUID, createHmac } from "crypto";
|
|
19
|
+
import { EventEmitter } from "events";
|
|
13
20
|
class ShokupanResponse {
|
|
14
|
-
_headers =
|
|
21
|
+
_headers = null;
|
|
15
22
|
_status = 200;
|
|
16
23
|
/**
|
|
17
24
|
* Get the current headers
|
|
18
25
|
*/
|
|
19
26
|
get headers() {
|
|
27
|
+
if (!this._headers) this._headers = new Headers();
|
|
20
28
|
return this._headers;
|
|
21
29
|
}
|
|
22
30
|
/**
|
|
@@ -37,6 +45,7 @@ class ShokupanResponse {
|
|
|
37
45
|
* @param value Header value
|
|
38
46
|
*/
|
|
39
47
|
set(key, value) {
|
|
48
|
+
if (!this._headers) this._headers = new Headers();
|
|
40
49
|
this._headers.set(key, value);
|
|
41
50
|
return this;
|
|
42
51
|
}
|
|
@@ -46,6 +55,7 @@ class ShokupanResponse {
|
|
|
46
55
|
* @param value Header value
|
|
47
56
|
*/
|
|
48
57
|
append(key, value) {
|
|
58
|
+
if (!this._headers) this._headers = new Headers();
|
|
49
59
|
this._headers.append(key, value);
|
|
50
60
|
return this;
|
|
51
61
|
}
|
|
@@ -54,29 +64,62 @@ class ShokupanResponse {
|
|
|
54
64
|
* @param key Header name
|
|
55
65
|
*/
|
|
56
66
|
get(key) {
|
|
57
|
-
return this._headers
|
|
67
|
+
return this._headers?.get(key) || null;
|
|
58
68
|
}
|
|
59
69
|
/**
|
|
60
70
|
* Check if a header exists
|
|
61
71
|
* @param key Header name
|
|
62
72
|
*/
|
|
63
73
|
has(key) {
|
|
64
|
-
return this._headers
|
|
74
|
+
return this._headers?.has(key) || false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Internal: check if headers have been initialized/modified
|
|
78
|
+
*/
|
|
79
|
+
get hasPopulatedHeaders() {
|
|
80
|
+
return this._headers !== null;
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
class ShokupanContext {
|
|
68
|
-
|
|
84
|
+
// Raw body for compression optimization
|
|
85
|
+
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
69
86
|
this.request = request;
|
|
70
87
|
this.server = server;
|
|
71
88
|
this.app = app;
|
|
72
|
-
this.
|
|
89
|
+
this.signal = signal;
|
|
73
90
|
this.state = state || {};
|
|
91
|
+
if (enableMiddlewareTracking) {
|
|
92
|
+
const self = this;
|
|
93
|
+
this.state = new Proxy(this.state, {
|
|
94
|
+
set(target, p, newValue, receiver) {
|
|
95
|
+
const result = Reflect.set(target, p, newValue, receiver);
|
|
96
|
+
const currentHandler = self.handlerStack[self.handlerStack.length - 1];
|
|
97
|
+
if (currentHandler) {
|
|
98
|
+
if (!currentHandler.stateChanges) currentHandler.stateChanges = {};
|
|
99
|
+
currentHandler.stateChanges[p] = newValue;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
74
105
|
this.response = new ShokupanResponse();
|
|
75
106
|
}
|
|
76
|
-
|
|
107
|
+
_url;
|
|
77
108
|
params = {};
|
|
109
|
+
// Router assigns this, but default to empty object
|
|
78
110
|
state;
|
|
111
|
+
handlerStack = [];
|
|
79
112
|
response;
|
|
113
|
+
_debug;
|
|
114
|
+
_finalResponse;
|
|
115
|
+
_rawBody;
|
|
116
|
+
get url() {
|
|
117
|
+
if (!this._url) {
|
|
118
|
+
const urlString = this.request.url || "http://localhost/";
|
|
119
|
+
this._url = new URL(urlString);
|
|
120
|
+
}
|
|
121
|
+
return this._url;
|
|
122
|
+
}
|
|
80
123
|
/**
|
|
81
124
|
* Base request
|
|
82
125
|
*/
|
|
@@ -93,13 +136,42 @@ class ShokupanContext {
|
|
|
93
136
|
* Request path
|
|
94
137
|
*/
|
|
95
138
|
get path() {
|
|
96
|
-
return this.
|
|
139
|
+
if (this._url) return this._url.pathname;
|
|
140
|
+
const url = this.request.url;
|
|
141
|
+
let queryIndex = url.indexOf("?");
|
|
142
|
+
const end = queryIndex === -1 ? url.length : queryIndex;
|
|
143
|
+
let start = 0;
|
|
144
|
+
const protocolIndex = url.indexOf("://");
|
|
145
|
+
if (protocolIndex !== -1) {
|
|
146
|
+
const hostStart = protocolIndex + 3;
|
|
147
|
+
const pathStart = url.indexOf("/", hostStart);
|
|
148
|
+
if (pathStart !== -1 && pathStart < end) {
|
|
149
|
+
start = pathStart;
|
|
150
|
+
} else {
|
|
151
|
+
return "/";
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
if (url.charCodeAt(0) === 47) {
|
|
155
|
+
start = 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return url.substring(start, end);
|
|
97
159
|
}
|
|
98
160
|
/**
|
|
99
161
|
* Request query params
|
|
100
162
|
*/
|
|
101
163
|
get query() {
|
|
102
|
-
|
|
164
|
+
const q = {};
|
|
165
|
+
for (const [key, value] of this.url.searchParams) {
|
|
166
|
+
if (q[key] === void 0) {
|
|
167
|
+
q[key] = value;
|
|
168
|
+
} else if (Array.isArray(q[key])) {
|
|
169
|
+
q[key].push(value);
|
|
170
|
+
} else {
|
|
171
|
+
q[key] = [q[key], value];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return q;
|
|
103
175
|
}
|
|
104
176
|
/**
|
|
105
177
|
* Client IP address
|
|
@@ -174,25 +246,60 @@ class ShokupanContext {
|
|
|
174
246
|
setCookie(name, value, options = {}) {
|
|
175
247
|
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
176
248
|
if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
249
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
250
|
+
if (options.path) cookie += `; Path=${options.path || "/"}`;
|
|
177
251
|
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
178
252
|
if (options.httpOnly) cookie += `; HttpOnly`;
|
|
179
253
|
if (options.secure) cookie += `; Secure`;
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
254
|
+
let sameSite = options.sameSite;
|
|
255
|
+
if (sameSite === true) sameSite = "Strict";
|
|
256
|
+
if (sameSite === void 0 || sameSite === false) ;
|
|
257
|
+
else {
|
|
258
|
+
const stringSameSite = typeof sameSite === "string" ? sameSite.toLowerCase() : sameSite;
|
|
259
|
+
switch (stringSameSite) {
|
|
260
|
+
case "lax":
|
|
261
|
+
cookie += "; SameSite=Lax";
|
|
262
|
+
break;
|
|
263
|
+
case "strict":
|
|
264
|
+
cookie += "; SameSite=Strict";
|
|
265
|
+
break;
|
|
266
|
+
case "none":
|
|
267
|
+
cookie += "; SameSite=None";
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
cookie += "; SameSite=Lax";
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
185
273
|
}
|
|
186
274
|
if (options.priority) {
|
|
187
|
-
|
|
275
|
+
const p = options.priority.toLowerCase();
|
|
276
|
+
if (p === "low") cookie += "; Priority=Low";
|
|
277
|
+
else if (p === "medium") cookie += "; Priority=Medium";
|
|
278
|
+
else if (p === "high") cookie += "; Priority=High";
|
|
188
279
|
}
|
|
189
280
|
this.response.append("Set-Cookie", cookie);
|
|
190
281
|
return this;
|
|
191
282
|
}
|
|
192
283
|
mergeHeaders(headers) {
|
|
193
|
-
|
|
284
|
+
let h;
|
|
285
|
+
if (this.response.hasPopulatedHeaders) {
|
|
286
|
+
h = new Headers(this.response.headers);
|
|
287
|
+
} else {
|
|
288
|
+
h = new Headers();
|
|
289
|
+
}
|
|
194
290
|
if (headers) {
|
|
195
|
-
|
|
291
|
+
if (headers instanceof Headers) {
|
|
292
|
+
headers.forEach((v, k) => h.set(k, v));
|
|
293
|
+
} else if (Array.isArray(headers)) {
|
|
294
|
+
headers.forEach(([k, v]) => h.set(k, v));
|
|
295
|
+
} else {
|
|
296
|
+
const keys = Object.keys(headers);
|
|
297
|
+
for (let i = 0; i < keys.length; i++) {
|
|
298
|
+
const key = keys[i];
|
|
299
|
+
const val = headers[key];
|
|
300
|
+
h.set(key, val);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
196
303
|
}
|
|
197
304
|
return h;
|
|
198
305
|
}
|
|
@@ -205,17 +312,21 @@ class ShokupanContext {
|
|
|
205
312
|
send(body, options) {
|
|
206
313
|
const headers = this.mergeHeaders(options?.headers);
|
|
207
314
|
const status = options?.status ?? this.response.status;
|
|
208
|
-
|
|
315
|
+
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
316
|
+
this._rawBody = body;
|
|
317
|
+
}
|
|
318
|
+
this._finalResponse = new Response(body, { status, headers });
|
|
319
|
+
return this._finalResponse;
|
|
209
320
|
}
|
|
210
321
|
/**
|
|
211
322
|
* Read request body
|
|
212
323
|
*/
|
|
213
324
|
async body() {
|
|
214
|
-
const contentType = this.request.headers.get("content-type");
|
|
215
|
-
if (contentType
|
|
325
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
326
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
216
327
|
return this.request.json();
|
|
217
328
|
}
|
|
218
|
-
if (contentType
|
|
329
|
+
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
219
330
|
return this.request.formData();
|
|
220
331
|
}
|
|
221
332
|
return this.request.text();
|
|
@@ -224,28 +335,49 @@ class ShokupanContext {
|
|
|
224
335
|
* Respond with a JSON object
|
|
225
336
|
*/
|
|
226
337
|
json(data, status, headers) {
|
|
338
|
+
const finalStatus = status ?? this.response.status;
|
|
339
|
+
const jsonString = JSON.stringify(data);
|
|
340
|
+
this._rawBody = jsonString;
|
|
341
|
+
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
342
|
+
this._finalResponse = new Response(jsonString, {
|
|
343
|
+
status: finalStatus,
|
|
344
|
+
headers: { "content-type": "application/json" }
|
|
345
|
+
});
|
|
346
|
+
return this._finalResponse;
|
|
347
|
+
}
|
|
227
348
|
const finalHeaders = this.mergeHeaders(headers);
|
|
228
349
|
finalHeaders.set("content-type", "application/json");
|
|
229
|
-
|
|
230
|
-
return
|
|
350
|
+
this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
|
|
351
|
+
return this._finalResponse;
|
|
231
352
|
}
|
|
232
353
|
/**
|
|
233
354
|
* Respond with a text string
|
|
234
355
|
*/
|
|
235
356
|
text(data, status, headers) {
|
|
236
|
-
const finalHeaders = this.mergeHeaders(headers);
|
|
237
|
-
finalHeaders.set("content-type", "text/plain");
|
|
238
357
|
const finalStatus = status ?? this.response.status;
|
|
239
|
-
|
|
358
|
+
this._rawBody = data;
|
|
359
|
+
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
360
|
+
this._finalResponse = new Response(data, {
|
|
361
|
+
status: finalStatus,
|
|
362
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
363
|
+
});
|
|
364
|
+
return this._finalResponse;
|
|
365
|
+
}
|
|
366
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
367
|
+
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
368
|
+
this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
369
|
+
return this._finalResponse;
|
|
240
370
|
}
|
|
241
371
|
/**
|
|
242
372
|
* Respond with HTML content
|
|
243
373
|
*/
|
|
244
374
|
html(html, status, headers) {
|
|
245
|
-
const finalHeaders = this.mergeHeaders(headers);
|
|
246
|
-
finalHeaders.set("content-type", "text/html");
|
|
247
375
|
const finalStatus = status ?? this.response.status;
|
|
248
|
-
|
|
376
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
377
|
+
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
378
|
+
this._rawBody = html;
|
|
379
|
+
this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
380
|
+
return this._finalResponse;
|
|
249
381
|
}
|
|
250
382
|
/**
|
|
251
383
|
* Respond with a redirect
|
|
@@ -253,7 +385,8 @@ class ShokupanContext {
|
|
|
253
385
|
redirect(url, status = 302) {
|
|
254
386
|
const headers = this.mergeHeaders();
|
|
255
387
|
headers.set("Location", url);
|
|
256
|
-
|
|
388
|
+
this._finalResponse = new Response(null, { status, headers });
|
|
389
|
+
return this._finalResponse;
|
|
257
390
|
}
|
|
258
391
|
/**
|
|
259
392
|
* Respond with a status code
|
|
@@ -261,15 +394,26 @@ class ShokupanContext {
|
|
|
261
394
|
*/
|
|
262
395
|
status(status) {
|
|
263
396
|
const headers = this.mergeHeaders();
|
|
264
|
-
|
|
397
|
+
this._finalResponse = new Response(null, { status, headers });
|
|
398
|
+
return this._finalResponse;
|
|
265
399
|
}
|
|
266
400
|
/**
|
|
267
401
|
* Respond with a file
|
|
268
402
|
*/
|
|
269
|
-
file(path, fileOptions, responseOptions) {
|
|
403
|
+
async file(path, fileOptions, responseOptions) {
|
|
270
404
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
271
405
|
const status = responseOptions?.status ?? this.response.status;
|
|
272
|
-
|
|
406
|
+
if (typeof Bun !== "undefined") {
|
|
407
|
+
this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
|
|
408
|
+
return this._finalResponse;
|
|
409
|
+
} else {
|
|
410
|
+
const fileBuffer = await readFile(path);
|
|
411
|
+
if (fileOptions?.type) {
|
|
412
|
+
headers.set("content-type", fileOptions.type);
|
|
413
|
+
}
|
|
414
|
+
this._finalResponse = new Response(fileBuffer, { status, headers });
|
|
415
|
+
return this._finalResponse;
|
|
416
|
+
}
|
|
273
417
|
}
|
|
274
418
|
/**
|
|
275
419
|
* JSX Rendering Function
|
|
@@ -289,6 +433,74 @@ class ShokupanContext {
|
|
|
289
433
|
return this.html(html, status, headers);
|
|
290
434
|
}
|
|
291
435
|
}
|
|
436
|
+
function RateLimitMiddleware(options = {}) {
|
|
437
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
438
|
+
const max = options.limit || options.max || 5;
|
|
439
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
440
|
+
const statusCode = options.statusCode || 429;
|
|
441
|
+
const headers = options.headers !== false;
|
|
442
|
+
const mode = options.mode || "user";
|
|
443
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
444
|
+
if (mode === "absolute") {
|
|
445
|
+
return "global";
|
|
446
|
+
}
|
|
447
|
+
return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
448
|
+
});
|
|
449
|
+
const skip = options.skip || (() => false);
|
|
450
|
+
const hits = /* @__PURE__ */ new Map();
|
|
451
|
+
const interval = setInterval(() => {
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
for (const [key, record] of hits.entries()) {
|
|
454
|
+
if (record.resetTime <= now) {
|
|
455
|
+
hits.delete(key);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}, windowMs);
|
|
459
|
+
if (interval.unref) interval.unref();
|
|
460
|
+
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
461
|
+
if (skip(ctx)) return next();
|
|
462
|
+
const key = keyGenerator(ctx);
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
let record = hits.get(key);
|
|
465
|
+
if (!record || record.resetTime <= now) {
|
|
466
|
+
record = {
|
|
467
|
+
hits: 0,
|
|
468
|
+
resetTime: now + windowMs
|
|
469
|
+
};
|
|
470
|
+
hits.set(key, record);
|
|
471
|
+
}
|
|
472
|
+
record.hits++;
|
|
473
|
+
const remaining = Math.max(0, max - record.hits);
|
|
474
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
475
|
+
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
476
|
+
const setHeaders = (res) => {
|
|
477
|
+
if (!headers || !res || !res.headers) return;
|
|
478
|
+
try {
|
|
479
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
480
|
+
res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
481
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
482
|
+
} catch (e) {
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
if (record.hits > max) {
|
|
486
|
+
typeof message === "object" ? JSON.stringify(message) : String(message);
|
|
487
|
+
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
488
|
+
if (headers) {
|
|
489
|
+
setHeaders(res);
|
|
490
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
491
|
+
}
|
|
492
|
+
return res;
|
|
493
|
+
}
|
|
494
|
+
const response = await next();
|
|
495
|
+
if (response instanceof Response && headers) {
|
|
496
|
+
setHeaders(response);
|
|
497
|
+
}
|
|
498
|
+
return response;
|
|
499
|
+
};
|
|
500
|
+
rateLimitMiddleware.isBuiltin = true;
|
|
501
|
+
rateLimitMiddleware.pluginName = "RateLimit";
|
|
502
|
+
return rateLimitMiddleware;
|
|
503
|
+
}
|
|
292
504
|
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
293
505
|
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
294
506
|
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
@@ -385,6 +597,9 @@ const Patch = createMethodDecorator("PATCH");
|
|
|
385
597
|
const Options = createMethodDecorator("OPTIONS");
|
|
386
598
|
const Head = createMethodDecorator("HEAD");
|
|
387
599
|
const All = createMethodDecorator("ALL");
|
|
600
|
+
function RateLimit(options) {
|
|
601
|
+
return Use(RateLimitMiddleware(options));
|
|
602
|
+
}
|
|
388
603
|
class Container {
|
|
389
604
|
static services = /* @__PURE__ */ new Map();
|
|
390
605
|
static register(target, instance) {
|
|
@@ -418,69 +633,43 @@ function Inject(token) {
|
|
|
418
633
|
});
|
|
419
634
|
};
|
|
420
635
|
}
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
function traceHandler(fn, name) {
|
|
446
|
-
return async function(...args) {
|
|
447
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
448
|
-
kind: SpanKind.INTERNAL,
|
|
449
|
-
attributes: {
|
|
450
|
-
"http.route": name,
|
|
451
|
-
"component": "shokupan.route"
|
|
452
|
-
}
|
|
453
|
-
}, async (span) => {
|
|
636
|
+
const compose = (middleware) => {
|
|
637
|
+
if (!middleware.length) {
|
|
638
|
+
return (context2, next) => {
|
|
639
|
+
return next ? next() : Promise.resolve();
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
return function dispatch(context2, next) {
|
|
643
|
+
let index = -1;
|
|
644
|
+
async function runner(i) {
|
|
645
|
+
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
646
|
+
index = i;
|
|
647
|
+
if (i >= middleware.length) {
|
|
648
|
+
return next ? next() : Promise.resolve();
|
|
649
|
+
}
|
|
650
|
+
const fn = middleware[i];
|
|
651
|
+
if (!context2._debug) {
|
|
652
|
+
return fn(context2, () => runner(i + 1));
|
|
653
|
+
}
|
|
654
|
+
const debug = context2._debug;
|
|
655
|
+
const debugId = fn._debugId || fn.name || "anonymous";
|
|
656
|
+
const previousNode = debug.getCurrentNode();
|
|
657
|
+
debug.trackEdge(previousNode, debugId);
|
|
658
|
+
debug.setNode(debugId);
|
|
659
|
+
const start = performance.now();
|
|
454
660
|
try {
|
|
455
|
-
const
|
|
456
|
-
|
|
661
|
+
const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
|
|
662
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "success");
|
|
663
|
+
return res;
|
|
457
664
|
} catch (err) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
throw err;
|
|
665
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
|
|
666
|
+
return Promise.reject(err);
|
|
461
667
|
} finally {
|
|
462
|
-
|
|
668
|
+
if (previousNode) debug.setNode(previousNode);
|
|
463
669
|
}
|
|
464
|
-
});
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
const compose = (middleware) => {
|
|
468
|
-
function fn(context2, next) {
|
|
469
|
-
let runner = next || (async () => {
|
|
470
|
-
});
|
|
471
|
-
for (let i = middleware.length - 1; i >= 0; i--) {
|
|
472
|
-
const fn2 = traceMiddleware(middleware[i]);
|
|
473
|
-
const nextStep = runner;
|
|
474
|
-
let called = false;
|
|
475
|
-
runner = async () => {
|
|
476
|
-
if (called) throw new Error("next() called multiple times");
|
|
477
|
-
called = true;
|
|
478
|
-
return fn2(context2, nextStep);
|
|
479
|
-
};
|
|
480
670
|
}
|
|
481
|
-
return runner();
|
|
482
|
-
}
|
|
483
|
-
return fn;
|
|
671
|
+
return runner(0);
|
|
672
|
+
};
|
|
484
673
|
};
|
|
485
674
|
class ShokupanRequestBase {
|
|
486
675
|
method;
|
|
@@ -538,6 +727,15 @@ function deepMerge(target, ...sources) {
|
|
|
538
727
|
}
|
|
539
728
|
return deepMerge(target, ...sources);
|
|
540
729
|
}
|
|
730
|
+
const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
|
|
731
|
+
const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
|
|
732
|
+
const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
|
|
733
|
+
const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
|
|
734
|
+
const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
|
|
735
|
+
const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
|
|
736
|
+
const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
|
|
737
|
+
const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
|
|
738
|
+
const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
|
|
541
739
|
function analyzeHandler(handler) {
|
|
542
740
|
const handlerSource = handler.toString();
|
|
543
741
|
const inferredSpec = {};
|
|
@@ -547,46 +745,28 @@ function analyzeHandler(handler) {
|
|
|
547
745
|
};
|
|
548
746
|
}
|
|
549
747
|
const queryParams = /* @__PURE__ */ new Map();
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
queryIntMatch.forEach((match) => {
|
|
553
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
554
|
-
if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
|
|
555
|
-
});
|
|
748
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
|
|
749
|
+
if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
|
|
556
750
|
}
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
queryFloatMatch.forEach((match) => {
|
|
560
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
561
|
-
if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
|
|
562
|
-
});
|
|
751
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
|
|
752
|
+
if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
|
|
563
753
|
}
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (paramName && !queryParams.has(paramName)) {
|
|
569
|
-
queryParams.set(paramName, { type: "number" });
|
|
570
|
-
}
|
|
571
|
-
});
|
|
754
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
|
|
755
|
+
if (match[1] && !queryParams.has(match[1])) {
|
|
756
|
+
queryParams.set(match[1], { type: "number" });
|
|
757
|
+
}
|
|
572
758
|
}
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
queryParams.set(paramName, { type: "boolean" });
|
|
579
|
-
}
|
|
580
|
-
});
|
|
759
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
|
|
760
|
+
const name = match[1] || match[2];
|
|
761
|
+
if (name && !queryParams.has(name)) {
|
|
762
|
+
queryParams.set(name, { type: "boolean" });
|
|
763
|
+
}
|
|
581
764
|
}
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
queryParams.set(paramName, { type: "string" });
|
|
588
|
-
}
|
|
589
|
-
});
|
|
765
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
|
|
766
|
+
const name = match[1];
|
|
767
|
+
if (name && !queryParams.has(name)) {
|
|
768
|
+
queryParams.set(name, { type: "string" });
|
|
769
|
+
}
|
|
590
770
|
}
|
|
591
771
|
if (queryParams.size > 0) {
|
|
592
772
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -599,19 +779,11 @@ function analyzeHandler(handler) {
|
|
|
599
779
|
});
|
|
600
780
|
}
|
|
601
781
|
const pathParams = /* @__PURE__ */ new Map();
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
paramIntMatch.forEach((match) => {
|
|
605
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
606
|
-
if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
|
|
607
|
-
});
|
|
782
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
|
|
783
|
+
if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
|
|
608
784
|
}
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
paramFloatMatch.forEach((match) => {
|
|
612
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
613
|
-
if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
|
|
614
|
-
});
|
|
785
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
|
|
786
|
+
if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
|
|
615
787
|
}
|
|
616
788
|
if (pathParams.size > 0) {
|
|
617
789
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -624,76 +796,55 @@ function analyzeHandler(handler) {
|
|
|
624
796
|
});
|
|
625
797
|
});
|
|
626
798
|
}
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
schema: { type: "string" }
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
});
|
|
799
|
+
for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
|
|
800
|
+
if (match[1]) {
|
|
801
|
+
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
802
|
+
inferredSpec.parameters.push({
|
|
803
|
+
name: match[1],
|
|
804
|
+
in: "header",
|
|
805
|
+
schema: { type: "string" }
|
|
806
|
+
});
|
|
807
|
+
}
|
|
640
808
|
}
|
|
641
809
|
const responses = {};
|
|
642
810
|
if (handlerSource.includes("ctx.json(")) {
|
|
643
811
|
responses["200"] = {
|
|
644
812
|
description: "Successful response",
|
|
645
|
-
content: {
|
|
646
|
-
"application/json": { schema: { type: "object" } }
|
|
647
|
-
}
|
|
813
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
648
814
|
};
|
|
649
815
|
}
|
|
650
816
|
if (handlerSource.includes("ctx.html(")) {
|
|
651
817
|
responses["200"] = {
|
|
652
818
|
description: "Successful response",
|
|
653
|
-
content: {
|
|
654
|
-
"text/html": { schema: { type: "string" } }
|
|
655
|
-
}
|
|
819
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
656
820
|
};
|
|
657
821
|
}
|
|
658
822
|
if (handlerSource.includes("ctx.text(")) {
|
|
659
823
|
responses["200"] = {
|
|
660
824
|
description: "Successful response",
|
|
661
|
-
content: {
|
|
662
|
-
"text/plain": { schema: { type: "string" } }
|
|
663
|
-
}
|
|
825
|
+
content: { "text/plain": { schema: { type: "string" } } }
|
|
664
826
|
};
|
|
665
827
|
}
|
|
666
828
|
if (handlerSource.includes("ctx.file(")) {
|
|
667
829
|
responses["200"] = {
|
|
668
830
|
description: "File download",
|
|
669
|
-
content: {
|
|
670
|
-
"application/octet-stream": { schema: { type: "string", format: "binary" } }
|
|
671
|
-
}
|
|
831
|
+
content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
|
|
672
832
|
};
|
|
673
833
|
}
|
|
674
834
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
675
|
-
responses["302"] = {
|
|
676
|
-
description: "Redirect"
|
|
677
|
-
};
|
|
835
|
+
responses["302"] = { description: "Redirect" };
|
|
678
836
|
}
|
|
679
837
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
680
838
|
responses["200"] = {
|
|
681
839
|
description: "Successful response",
|
|
682
|
-
content: {
|
|
683
|
-
"application/json": { schema: { type: "object" } }
|
|
684
|
-
}
|
|
840
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
685
841
|
};
|
|
686
842
|
}
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
responses[statusCode] = {
|
|
693
|
-
description: `Error response (${statusCode})`
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
});
|
|
843
|
+
for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
|
|
844
|
+
const statusCode = match[1];
|
|
845
|
+
if (statusCode && statusCode !== "200") {
|
|
846
|
+
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
847
|
+
}
|
|
697
848
|
}
|
|
698
849
|
if (Object.keys(responses).length > 0) {
|
|
699
850
|
inferredSpec.responses = responses;
|
|
@@ -707,7 +858,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
707
858
|
const defaultTagName = options.defaultTag || "Application";
|
|
708
859
|
let astRoutes = [];
|
|
709
860
|
try {
|
|
710
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-
|
|
861
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-BtIaHIfe.js");
|
|
711
862
|
const analyzer = new OpenAPIAnalyzer2(process.cwd());
|
|
712
863
|
const { applications } = await analyzer.analyze();
|
|
713
864
|
const appMap = /* @__PURE__ */ new Map();
|
|
@@ -977,10 +1128,10 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
977
1128
|
};
|
|
978
1129
|
}
|
|
979
1130
|
const eta$1 = new Eta();
|
|
980
|
-
function serveStatic(
|
|
1131
|
+
function serveStatic(config, prefix) {
|
|
981
1132
|
const rootPath = resolve(config.root || ".");
|
|
982
1133
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
983
|
-
|
|
1134
|
+
const serveStaticMiddleware = async (ctx) => {
|
|
984
1135
|
let relative = ctx.path.slice(normalizedPrefix.length);
|
|
985
1136
|
if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
|
|
986
1137
|
if (relative.length === 0) relative = "/";
|
|
@@ -1097,16 +1248,209 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1097
1248
|
}
|
|
1098
1249
|
}
|
|
1099
1250
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1251
|
+
let response;
|
|
1252
|
+
if (typeof Bun !== "undefined") {
|
|
1253
|
+
response = new Response(Bun.file(finalPath));
|
|
1254
|
+
} else {
|
|
1255
|
+
const fileBuffer = await readFile$1(finalPath);
|
|
1256
|
+
response = new Response(fileBuffer);
|
|
1257
|
+
}
|
|
1102
1258
|
if (config.hooks?.onResponse) {
|
|
1103
1259
|
const hooked = await config.hooks.onResponse(ctx, response);
|
|
1104
1260
|
if (hooked) response = hooked;
|
|
1105
1261
|
}
|
|
1106
1262
|
return response;
|
|
1107
1263
|
};
|
|
1264
|
+
serveStaticMiddleware.isBuiltin = true;
|
|
1265
|
+
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1266
|
+
return serveStaticMiddleware;
|
|
1267
|
+
}
|
|
1268
|
+
class RouterTrie {
|
|
1269
|
+
root;
|
|
1270
|
+
constructor() {
|
|
1271
|
+
this.root = this.createNode();
|
|
1272
|
+
}
|
|
1273
|
+
createNode() {
|
|
1274
|
+
return {
|
|
1275
|
+
children: {}
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
insert(method, path, handler) {
|
|
1279
|
+
let node = this.root;
|
|
1280
|
+
const segments = this.splitPath(path);
|
|
1281
|
+
for (const segment of segments) {
|
|
1282
|
+
if (segment === "**") {
|
|
1283
|
+
if (!node.recursiveChild) {
|
|
1284
|
+
node.recursiveChild = this.createNode();
|
|
1285
|
+
}
|
|
1286
|
+
node = node.recursiveChild;
|
|
1287
|
+
} else if (segment === "*") {
|
|
1288
|
+
if (!node.wildcardChild) {
|
|
1289
|
+
node.wildcardChild = this.createNode();
|
|
1290
|
+
}
|
|
1291
|
+
node = node.wildcardChild;
|
|
1292
|
+
} else if (segment.startsWith(":")) {
|
|
1293
|
+
const paramName = segment.slice(1);
|
|
1294
|
+
if (!node.paramChild) {
|
|
1295
|
+
node.paramChild = this.createNode();
|
|
1296
|
+
node.paramChild.paramName = paramName;
|
|
1297
|
+
}
|
|
1298
|
+
node = node.paramChild;
|
|
1299
|
+
node.paramName = paramName;
|
|
1300
|
+
} else {
|
|
1301
|
+
if (!node.children[segment]) {
|
|
1302
|
+
node.children[segment] = this.createNode();
|
|
1303
|
+
}
|
|
1304
|
+
node = node.children[segment];
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (!node.handlers) {
|
|
1308
|
+
node.handlers = {};
|
|
1309
|
+
}
|
|
1310
|
+
node.handlers[method] = handler;
|
|
1311
|
+
}
|
|
1312
|
+
search(method, path) {
|
|
1313
|
+
const segments = this.splitPath(path);
|
|
1314
|
+
const params = {};
|
|
1315
|
+
const match = this.findNode(this.root, segments, 0, params);
|
|
1316
|
+
if (match && match.handlers) {
|
|
1317
|
+
const handler = match.handlers[method] || match.handlers["ALL"];
|
|
1318
|
+
if (handler) {
|
|
1319
|
+
return { handler, params };
|
|
1320
|
+
}
|
|
1321
|
+
if (method === "HEAD" && match.handlers["GET"]) {
|
|
1322
|
+
return { handler: match.handlers["GET"], params };
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
findNode(node, segments, index, params) {
|
|
1328
|
+
if (index === segments.length) {
|
|
1329
|
+
if (node.handlers) return node;
|
|
1330
|
+
if (node.recursiveChild && node.recursiveChild.handlers) {
|
|
1331
|
+
return node.recursiveChild;
|
|
1332
|
+
}
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
const segment = segments[index];
|
|
1336
|
+
const child = node.children[segment];
|
|
1337
|
+
if (child) {
|
|
1338
|
+
const result = this.findNode(child, segments, index + 1, params);
|
|
1339
|
+
if (result) return result;
|
|
1340
|
+
}
|
|
1341
|
+
if (node.paramChild) {
|
|
1342
|
+
params[node.paramChild.paramName] = segment;
|
|
1343
|
+
const result = this.findNode(node.paramChild, segments, index + 1, params);
|
|
1344
|
+
if (result) return result;
|
|
1345
|
+
delete params[node.paramChild.paramName];
|
|
1346
|
+
}
|
|
1347
|
+
if (node.wildcardChild) {
|
|
1348
|
+
const result = this.findNode(node.wildcardChild, segments, index + 1, params);
|
|
1349
|
+
if (result) return result;
|
|
1350
|
+
}
|
|
1351
|
+
if (node.recursiveChild) {
|
|
1352
|
+
const remaining = segments.length - index;
|
|
1353
|
+
for (let k = 0; k <= remaining; k++) {
|
|
1354
|
+
const result = this.findNode(node.recursiveChild, segments, index + k, params);
|
|
1355
|
+
if (result) return result;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
splitPath(path) {
|
|
1361
|
+
if (path === "/" || path === "") return [];
|
|
1362
|
+
const s = path.startsWith("/") ? path.slice(1) : path;
|
|
1363
|
+
if (s === "") return [];
|
|
1364
|
+
return s.split("/");
|
|
1365
|
+
}
|
|
1108
1366
|
}
|
|
1109
1367
|
const asyncContext = new AsyncLocalStorage();
|
|
1368
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1369
|
+
const db = new Surreal({
|
|
1370
|
+
engines: createNodeEngines()
|
|
1371
|
+
});
|
|
1372
|
+
const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
|
|
1373
|
+
return db.query(`
|
|
1374
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1375
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1376
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1377
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1378
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1379
|
+
`);
|
|
1380
|
+
});
|
|
1381
|
+
const datastore = {
|
|
1382
|
+
get(store, key) {
|
|
1383
|
+
return db.select(new RecordId(store, key));
|
|
1384
|
+
},
|
|
1385
|
+
set(store, key, value) {
|
|
1386
|
+
return db.create(new RecordId(store, key)).content(value);
|
|
1387
|
+
},
|
|
1388
|
+
async query(query, vars) {
|
|
1389
|
+
try {
|
|
1390
|
+
const r = await db.query(query, vars).collect();
|
|
1391
|
+
return r;
|
|
1392
|
+
} catch (e) {
|
|
1393
|
+
console.error("DS ERROR:", e);
|
|
1394
|
+
throw e;
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
ready
|
|
1398
|
+
};
|
|
1399
|
+
process.on("exit", async () => {
|
|
1400
|
+
await db.close();
|
|
1401
|
+
});
|
|
1402
|
+
const tracer = trace.getTracer("shokupan.middleware");
|
|
1403
|
+
function traceHandler(fn, name) {
|
|
1404
|
+
return async function(...args) {
|
|
1405
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1406
|
+
kind: SpanKind.INTERNAL,
|
|
1407
|
+
attributes: {
|
|
1408
|
+
"http.route": name,
|
|
1409
|
+
"component": "shokupan.route"
|
|
1410
|
+
}
|
|
1411
|
+
}, async (span) => {
|
|
1412
|
+
try {
|
|
1413
|
+
const result = await fn.apply(this, args);
|
|
1414
|
+
return result;
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
span.recordException(err);
|
|
1417
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
1418
|
+
throw err;
|
|
1419
|
+
} finally {
|
|
1420
|
+
span.end();
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function getCallerInfo(skipFrames = 1) {
|
|
1426
|
+
let file = "unknown";
|
|
1427
|
+
let line = 0;
|
|
1428
|
+
try {
|
|
1429
|
+
const err = new Error();
|
|
1430
|
+
const stack = err.stack?.split("\n") || [];
|
|
1431
|
+
let found = 0;
|
|
1432
|
+
for (let i = 1; i < stack.length; i++) {
|
|
1433
|
+
const l = stack[i];
|
|
1434
|
+
if (!l.includes(":")) continue;
|
|
1435
|
+
if (l.includes("node_modules")) continue;
|
|
1436
|
+
if (l.includes("bun:main")) continue;
|
|
1437
|
+
if (l.includes("src/util/stack.ts")) continue;
|
|
1438
|
+
if (l.includes("src/router.ts")) continue;
|
|
1439
|
+
if (l.includes("src/shokupan.ts")) continue;
|
|
1440
|
+
found++;
|
|
1441
|
+
if (found >= skipFrames) {
|
|
1442
|
+
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1443
|
+
if (match) {
|
|
1444
|
+
file = match[1];
|
|
1445
|
+
line = parseInt(match[2], 10);
|
|
1446
|
+
return { file, line };
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
} catch (e) {
|
|
1451
|
+
}
|
|
1452
|
+
return { file, line };
|
|
1453
|
+
}
|
|
1110
1454
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1111
1455
|
const ShokupanApplicationTree = {};
|
|
1112
1456
|
class ShokupanRouter {
|
|
@@ -1126,6 +1470,7 @@ class ShokupanRouter {
|
|
|
1126
1470
|
[$parent] = null;
|
|
1127
1471
|
[$childRouters] = [];
|
|
1128
1472
|
[$childControllers] = [];
|
|
1473
|
+
middleware = [];
|
|
1129
1474
|
get rootConfig() {
|
|
1130
1475
|
return this[$appRoot]?.applicationConfig;
|
|
1131
1476
|
}
|
|
@@ -1134,7 +1479,54 @@ class ShokupanRouter {
|
|
|
1134
1479
|
}
|
|
1135
1480
|
[$routes] = [];
|
|
1136
1481
|
// Public via Symbol for OpenAPI generator
|
|
1482
|
+
trie = new RouterTrie();
|
|
1483
|
+
metadata;
|
|
1484
|
+
// Metadata for the router itself
|
|
1137
1485
|
currentGuards = [];
|
|
1486
|
+
// Registry Accessor
|
|
1487
|
+
getComponentRegistry() {
|
|
1488
|
+
const routes = this[$routes].map((r) => ({
|
|
1489
|
+
type: "route",
|
|
1490
|
+
path: r.path,
|
|
1491
|
+
method: r.method,
|
|
1492
|
+
metadata: r.metadata,
|
|
1493
|
+
handlerName: r.handler.name,
|
|
1494
|
+
tags: r.handlerSpec?.tags,
|
|
1495
|
+
order: r.order,
|
|
1496
|
+
_fn: r.handler
|
|
1497
|
+
// Expose handler for debugging instrumentation
|
|
1498
|
+
}));
|
|
1499
|
+
const mw = this.middleware;
|
|
1500
|
+
const middleware = mw ? mw.map((m) => ({
|
|
1501
|
+
name: m.name || "middleware",
|
|
1502
|
+
metadata: m.metadata,
|
|
1503
|
+
order: m.order,
|
|
1504
|
+
_fn: m
|
|
1505
|
+
// Expose function for debugging instrumentation
|
|
1506
|
+
})) : [];
|
|
1507
|
+
const routers = this[$childRouters].map((r) => ({
|
|
1508
|
+
type: "router",
|
|
1509
|
+
path: r[$mountPath],
|
|
1510
|
+
metadata: r.metadata,
|
|
1511
|
+
children: r.getComponentRegistry()
|
|
1512
|
+
}));
|
|
1513
|
+
const controllers = this[$childControllers].map((c) => {
|
|
1514
|
+
return {
|
|
1515
|
+
type: "controller",
|
|
1516
|
+
path: c[$mountPath] || "/",
|
|
1517
|
+
name: c.constructor.name,
|
|
1518
|
+
metadata: c.metadata
|
|
1519
|
+
// Check if we can store this
|
|
1520
|
+
};
|
|
1521
|
+
});
|
|
1522
|
+
return {
|
|
1523
|
+
metadata: this.metadata,
|
|
1524
|
+
middleware,
|
|
1525
|
+
routes,
|
|
1526
|
+
routers,
|
|
1527
|
+
controllers
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1138
1530
|
isRouterInstance(target) {
|
|
1139
1531
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1140
1532
|
}
|
|
@@ -1160,6 +1552,14 @@ class ShokupanRouter {
|
|
|
1160
1552
|
throw new Error("Router is already mounted");
|
|
1161
1553
|
}
|
|
1162
1554
|
controller[$mountPath] = prefix;
|
|
1555
|
+
if (!controller.metadata) {
|
|
1556
|
+
const info = getCallerInfo();
|
|
1557
|
+
controller.metadata = {
|
|
1558
|
+
file: info.file,
|
|
1559
|
+
line: info.line,
|
|
1560
|
+
name: "MountedRouter"
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1163
1563
|
this[$childRouters].push(controller);
|
|
1164
1564
|
controller[$parent] = this;
|
|
1165
1565
|
const setRouterContext = (router) => {
|
|
@@ -1192,6 +1592,12 @@ class ShokupanRouter {
|
|
|
1192
1592
|
}
|
|
1193
1593
|
}
|
|
1194
1594
|
instance[$mountPath] = prefix;
|
|
1595
|
+
const info = getCallerInfo();
|
|
1596
|
+
instance.metadata = {
|
|
1597
|
+
file: info.file,
|
|
1598
|
+
line: info.line,
|
|
1599
|
+
name: instance.constructor.name
|
|
1600
|
+
};
|
|
1195
1601
|
this[$childControllers].push(instance);
|
|
1196
1602
|
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1197
1603
|
const proto = Object.getPrototypeOf(instance);
|
|
@@ -1275,14 +1681,39 @@ class ShokupanRouter {
|
|
|
1275
1681
|
for (const arg of sortedArgs) {
|
|
1276
1682
|
switch (arg.type) {
|
|
1277
1683
|
case RouteParamType.BODY:
|
|
1278
|
-
|
|
1684
|
+
try {
|
|
1685
|
+
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
1686
|
+
args[arg.index] = await ctx.req.json();
|
|
1687
|
+
} else {
|
|
1688
|
+
const text = await ctx.req.text();
|
|
1689
|
+
if (!text) {
|
|
1690
|
+
args[arg.index] = {};
|
|
1691
|
+
} else {
|
|
1692
|
+
args[arg.index] = JSON.parse(text);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
} catch (e) {
|
|
1696
|
+
const err = new Error("Invalid JSON body");
|
|
1697
|
+
err.status = 400;
|
|
1698
|
+
throw err;
|
|
1699
|
+
}
|
|
1279
1700
|
break;
|
|
1280
1701
|
case RouteParamType.PARAM:
|
|
1281
1702
|
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
1282
1703
|
break;
|
|
1283
1704
|
case RouteParamType.QUERY: {
|
|
1284
1705
|
const url = new URL(ctx.req.url);
|
|
1285
|
-
|
|
1706
|
+
if (arg.name) {
|
|
1707
|
+
const vals = url.searchParams.getAll(arg.name);
|
|
1708
|
+
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1709
|
+
} else {
|
|
1710
|
+
const query = {};
|
|
1711
|
+
for (const key of url.searchParams.keys()) {
|
|
1712
|
+
const vals = url.searchParams.getAll(key);
|
|
1713
|
+
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1714
|
+
}
|
|
1715
|
+
args[arg.index] = query;
|
|
1716
|
+
}
|
|
1286
1717
|
break;
|
|
1287
1718
|
}
|
|
1288
1719
|
case RouteParamType.HEADER:
|
|
@@ -1297,7 +1728,7 @@ class ShokupanRouter {
|
|
|
1297
1728
|
}
|
|
1298
1729
|
}
|
|
1299
1730
|
}
|
|
1300
|
-
const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
|
|
1731
|
+
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
1301
1732
|
return tracedOriginalHandler.apply(instance, args);
|
|
1302
1733
|
};
|
|
1303
1734
|
let finalHandler = wrappedHandler;
|
|
@@ -1430,29 +1861,59 @@ class ShokupanRouter {
|
|
|
1430
1861
|
data: result
|
|
1431
1862
|
};
|
|
1432
1863
|
}
|
|
1433
|
-
|
|
1864
|
+
applyRouterHooks(match) {
|
|
1434
1865
|
if (!this.config?.hooks) return match;
|
|
1435
1866
|
const hooks = this.config.hooks;
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1867
|
+
return {
|
|
1868
|
+
...match,
|
|
1869
|
+
handler: this.wrapWithHooks(match.handler, hooks)
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
wrapWithHooks(handler, hooks) {
|
|
1873
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1874
|
+
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1875
|
+
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1876
|
+
const hasError = hookList.some((h) => !!h.onError);
|
|
1877
|
+
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1878
|
+
const originalHandler = handler;
|
|
1879
|
+
const wrapped = async (ctx) => {
|
|
1880
|
+
if (hasStart) {
|
|
1881
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1882
|
+
const h = hookList[i];
|
|
1883
|
+
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const debug = ctx._debug;
|
|
1887
|
+
let debugId;
|
|
1888
|
+
let previousNode;
|
|
1889
|
+
if (debug) {
|
|
1890
|
+
debugId = originalHandler._debugId || originalHandler.name || "handler";
|
|
1891
|
+
previousNode = debug.getCurrentNode();
|
|
1892
|
+
debug.trackEdge(previousNode, debugId);
|
|
1893
|
+
debug.setNode(debugId);
|
|
1894
|
+
}
|
|
1895
|
+
const start = performance.now();
|
|
1896
|
+
try {
|
|
1897
|
+
const res = await originalHandler(ctx);
|
|
1898
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1899
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1900
|
+
const h = hookList[i];
|
|
1901
|
+
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1902
|
+
}
|
|
1903
|
+
return res;
|
|
1443
1904
|
} catch (err) {
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
console.error("Error in router onError hook:", e);
|
|
1449
|
-
}
|
|
1905
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1906
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1907
|
+
const h = hookList[i];
|
|
1908
|
+
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1450
1909
|
}
|
|
1451
1910
|
throw err;
|
|
1911
|
+
} finally {
|
|
1912
|
+
if (debug && previousNode) debug.setNode(previousNode);
|
|
1452
1913
|
}
|
|
1453
1914
|
};
|
|
1454
|
-
|
|
1455
|
-
return
|
|
1915
|
+
wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
|
|
1916
|
+
return wrapped;
|
|
1456
1917
|
}
|
|
1457
1918
|
/**
|
|
1458
1919
|
* Find a route matching the given method and path.
|
|
@@ -1461,29 +1922,24 @@ class ShokupanRouter {
|
|
|
1461
1922
|
* @returns Route handler and parameters if found, otherwise null
|
|
1462
1923
|
*/
|
|
1463
1924
|
find(method, path) {
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
route.keys.forEach((key, index) => {
|
|
1470
|
-
params[key] = match[index + 1];
|
|
1471
|
-
});
|
|
1472
|
-
return this.applyHooks({ handler: route.handler, params });
|
|
1473
|
-
}
|
|
1925
|
+
let result = this.trie.search(method, path);
|
|
1926
|
+
if (result) return result;
|
|
1927
|
+
if (method === "HEAD") {
|
|
1928
|
+
result = this.trie.search("GET", path);
|
|
1929
|
+
if (result) return result;
|
|
1474
1930
|
}
|
|
1475
1931
|
for (const child of this[$childRouters]) {
|
|
1476
1932
|
const prefix = child[$mountPath];
|
|
1477
1933
|
if (path === prefix || path.startsWith(prefix + "/")) {
|
|
1478
1934
|
const subPath = path.slice(prefix.length) || "/";
|
|
1479
1935
|
const match = child.find(method, subPath);
|
|
1480
|
-
if (match) return this.
|
|
1936
|
+
if (match) return this.applyRouterHooks(match);
|
|
1481
1937
|
}
|
|
1482
1938
|
if (prefix.endsWith("/")) {
|
|
1483
1939
|
if (path.startsWith(prefix)) {
|
|
1484
1940
|
const subPath = path.slice(prefix.length) || "/";
|
|
1485
1941
|
const match = child.find(method, subPath);
|
|
1486
|
-
if (match) return this.
|
|
1942
|
+
if (match) return this.applyRouterHooks(match);
|
|
1487
1943
|
}
|
|
1488
1944
|
}
|
|
1489
1945
|
}
|
|
@@ -1494,7 +1950,7 @@ class ShokupanRouter {
|
|
|
1494
1950
|
const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1495
1951
|
keys.push(key);
|
|
1496
1952
|
return "([^/]+)";
|
|
1497
|
-
}).replace(
|
|
1953
|
+
}).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
|
|
1498
1954
|
return {
|
|
1499
1955
|
regex: new RegExp(`^${pattern}$`),
|
|
1500
1956
|
keys
|
|
@@ -1563,18 +2019,84 @@ class ShokupanRouter {
|
|
|
1563
2019
|
return innerHandler(ctx);
|
|
1564
2020
|
};
|
|
1565
2021
|
}
|
|
2022
|
+
const { file, line } = getCallerInfo();
|
|
2023
|
+
const trackingHandler = wrappedHandler;
|
|
2024
|
+
wrappedHandler = async (ctx) => {
|
|
2025
|
+
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2026
|
+
return trackingHandler(ctx);
|
|
2027
|
+
}
|
|
2028
|
+
const startTime = performance.now();
|
|
2029
|
+
let error = void 0;
|
|
2030
|
+
try {
|
|
2031
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2032
|
+
ctx.handlerStack.push({
|
|
2033
|
+
name: handler.name || "anonymous",
|
|
2034
|
+
file,
|
|
2035
|
+
line
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
return await trackingHandler(ctx);
|
|
2039
|
+
} catch (e) {
|
|
2040
|
+
error = e;
|
|
2041
|
+
throw e;
|
|
2042
|
+
} finally {
|
|
2043
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2044
|
+
const duration = performance.now() - startTime;
|
|
2045
|
+
const config = ctx.app.applicationConfig;
|
|
2046
|
+
try {
|
|
2047
|
+
const timestamp = Date.now();
|
|
2048
|
+
const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
|
|
2049
|
+
await datastore.set("middleware_tracking", key, {
|
|
2050
|
+
name: handler.name || "anonymous",
|
|
2051
|
+
path: ctx.path,
|
|
2052
|
+
timestamp,
|
|
2053
|
+
duration,
|
|
2054
|
+
file,
|
|
2055
|
+
line,
|
|
2056
|
+
error: error ? String(error) : void 0,
|
|
2057
|
+
metadata: {
|
|
2058
|
+
isBuiltin: handler.isBuiltin,
|
|
2059
|
+
pluginName: handler.pluginName
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2063
|
+
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2064
|
+
const cutoff = Date.now() - ttl;
|
|
2065
|
+
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2066
|
+
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2067
|
+
if (results && results[0] && results[0].count > maxCapacity) {
|
|
2068
|
+
const toDelete = results[0].count - maxCapacity;
|
|
2069
|
+
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2070
|
+
}
|
|
2071
|
+
} catch (datastoreError) {
|
|
2072
|
+
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2078
|
+
let bakedHandler = wrappedHandler;
|
|
2079
|
+
if (this.config?.hooks) {
|
|
2080
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
|
|
2081
|
+
}
|
|
1566
2082
|
this[$routes].push({
|
|
1567
2083
|
method,
|
|
1568
2084
|
path,
|
|
1569
|
-
regex,
|
|
1570
|
-
keys,
|
|
1571
|
-
handler
|
|
2085
|
+
regex: regex ?? new RegExp(""),
|
|
2086
|
+
keys: keys ?? [],
|
|
2087
|
+
handler,
|
|
2088
|
+
bakedHandler,
|
|
1572
2089
|
handlerSpec: spec,
|
|
1573
2090
|
group,
|
|
1574
|
-
|
|
1575
|
-
requestTimeout
|
|
1576
|
-
|
|
2091
|
+
hooks: this.config?.hooks,
|
|
2092
|
+
requestTimeout,
|
|
2093
|
+
renderer,
|
|
2094
|
+
metadata: {
|
|
2095
|
+
file,
|
|
2096
|
+
line
|
|
2097
|
+
}
|
|
1577
2098
|
});
|
|
2099
|
+
this.trie.insert(method, path, bakedHandler);
|
|
1578
2100
|
return this;
|
|
1579
2101
|
}
|
|
1580
2102
|
get(path, ...args) {
|
|
@@ -1608,7 +2130,35 @@ class ShokupanRouter {
|
|
|
1608
2130
|
guard(specOrHandler, handler) {
|
|
1609
2131
|
const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
|
|
1610
2132
|
const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
|
|
1611
|
-
|
|
2133
|
+
let file = "unknown";
|
|
2134
|
+
let line = 0;
|
|
2135
|
+
try {
|
|
2136
|
+
const err = new Error();
|
|
2137
|
+
const stack = err.stack?.split("\n") || [];
|
|
2138
|
+
const callerLine = stack.find(
|
|
2139
|
+
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
2140
|
+
);
|
|
2141
|
+
if (callerLine) {
|
|
2142
|
+
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
2143
|
+
if (match) {
|
|
2144
|
+
file = match[1];
|
|
2145
|
+
line = parseInt(match[2], 10);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
} catch (e) {
|
|
2149
|
+
}
|
|
2150
|
+
const trackedGuard = async (ctx, next) => {
|
|
2151
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2152
|
+
ctx.handlerStack.push({
|
|
2153
|
+
name: guardHandler.name || "guard",
|
|
2154
|
+
file,
|
|
2155
|
+
line
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
return guardHandler(ctx, next);
|
|
2159
|
+
};
|
|
2160
|
+
trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
|
|
2161
|
+
this.currentGuards.push({ handler: trackedGuard, spec });
|
|
1612
2162
|
return this;
|
|
1613
2163
|
}
|
|
1614
2164
|
/**
|
|
@@ -1620,10 +2170,10 @@ class ShokupanRouter {
|
|
|
1620
2170
|
const config = typeof options === "string" ? { root: options } : options;
|
|
1621
2171
|
const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
|
|
1622
2172
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1623
|
-
serveStatic(
|
|
2173
|
+
const handlerMiddleware = serveStatic(config, prefix);
|
|
1624
2174
|
const routeHandler = async (ctx) => {
|
|
1625
|
-
|
|
1626
|
-
|
|
2175
|
+
return handlerMiddleware(ctx, async () => {
|
|
2176
|
+
});
|
|
1627
2177
|
};
|
|
1628
2178
|
let groupName = "Static";
|
|
1629
2179
|
const segments = normalizedPrefix.split("/").filter(Boolean);
|
|
@@ -1684,6 +2234,49 @@ class ShokupanRouter {
|
|
|
1684
2234
|
return generateOpenApi(this, options);
|
|
1685
2235
|
}
|
|
1686
2236
|
}
|
|
2237
|
+
class SystemCpuMonitor {
|
|
2238
|
+
constructor(intervalMs = 1e3) {
|
|
2239
|
+
this.intervalMs = intervalMs;
|
|
2240
|
+
}
|
|
2241
|
+
interval = null;
|
|
2242
|
+
lastCpus = [];
|
|
2243
|
+
currentUsage = 0;
|
|
2244
|
+
start() {
|
|
2245
|
+
if (this.interval) return;
|
|
2246
|
+
this.lastCpus = os.cpus();
|
|
2247
|
+
this.interval = setInterval(() => this.update(), this.intervalMs);
|
|
2248
|
+
}
|
|
2249
|
+
stop() {
|
|
2250
|
+
if (this.interval) {
|
|
2251
|
+
clearInterval(this.interval);
|
|
2252
|
+
this.interval = null;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
getUsage() {
|
|
2256
|
+
return this.currentUsage;
|
|
2257
|
+
}
|
|
2258
|
+
update() {
|
|
2259
|
+
const cpus = os.cpus();
|
|
2260
|
+
let idle = 0;
|
|
2261
|
+
let total = 0;
|
|
2262
|
+
for (let i = 0; i < cpus.length; i++) {
|
|
2263
|
+
const cpu = cpus[i];
|
|
2264
|
+
const prev = this.lastCpus[i];
|
|
2265
|
+
let type;
|
|
2266
|
+
for (type in cpu.times) {
|
|
2267
|
+
const ticks = cpu.times[type];
|
|
2268
|
+
const prevTicks = prev.times[type];
|
|
2269
|
+
const diff = ticks - prevTicks;
|
|
2270
|
+
total += diff;
|
|
2271
|
+
if (type === "idle") {
|
|
2272
|
+
idle += diff;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
this.lastCpus = cpus;
|
|
2277
|
+
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
1687
2280
|
const defaults = {
|
|
1688
2281
|
port: 3e3,
|
|
1689
2282
|
hostname: "localhost",
|
|
@@ -1695,21 +2288,59 @@ trace.getTracer("shokupan.application");
|
|
|
1695
2288
|
class Shokupan extends ShokupanRouter {
|
|
1696
2289
|
applicationConfig = {};
|
|
1697
2290
|
openApiSpec;
|
|
1698
|
-
|
|
2291
|
+
composedMiddleware;
|
|
2292
|
+
cpuMonitor;
|
|
2293
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
2294
|
+
hooksInitialized = false;
|
|
1699
2295
|
get logger() {
|
|
1700
2296
|
return this.applicationConfig.logger;
|
|
1701
2297
|
}
|
|
1702
2298
|
constructor(applicationConfig = {}) {
|
|
1703
|
-
|
|
2299
|
+
const config = Object.assign({}, defaults, applicationConfig);
|
|
2300
|
+
const { hooks, ...routerConfig } = config;
|
|
2301
|
+
super(routerConfig);
|
|
1704
2302
|
this[$isApplication] = true;
|
|
1705
2303
|
this[$appRoot] = this;
|
|
1706
|
-
|
|
2304
|
+
this.applicationConfig = config;
|
|
2305
|
+
const { file, line } = getCallerInfo();
|
|
2306
|
+
this.metadata = {
|
|
2307
|
+
file,
|
|
2308
|
+
line,
|
|
2309
|
+
name: "ShokupanApplication"
|
|
2310
|
+
};
|
|
1707
2311
|
}
|
|
1708
2312
|
/**
|
|
1709
2313
|
* Adds middleware to the application.
|
|
1710
2314
|
*/
|
|
1711
2315
|
use(middleware) {
|
|
1712
|
-
|
|
2316
|
+
let trackedMiddleware = middleware;
|
|
2317
|
+
const { file, line } = getCallerInfo();
|
|
2318
|
+
if (!middleware.metadata) {
|
|
2319
|
+
middleware.metadata = {
|
|
2320
|
+
file,
|
|
2321
|
+
line,
|
|
2322
|
+
name: middleware.name || "middleware",
|
|
2323
|
+
isBuiltin: middleware.isBuiltin,
|
|
2324
|
+
pluginName: middleware.pluginName
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
trackedMiddleware = async (ctx, next) => {
|
|
2328
|
+
const c = ctx;
|
|
2329
|
+
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2330
|
+
const metadata = middleware.metadata || {};
|
|
2331
|
+
c.handlerStack.push({
|
|
2332
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2333
|
+
file: metadata.file || file,
|
|
2334
|
+
line: metadata.line || line,
|
|
2335
|
+
isBuiltin: metadata.isBuiltin
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
return middleware(ctx, next);
|
|
2339
|
+
};
|
|
2340
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2341
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2342
|
+
trackedMiddleware.order = this.middleware.length;
|
|
2343
|
+
this.middleware.push(trackedMiddleware);
|
|
1713
2344
|
return this;
|
|
1714
2345
|
}
|
|
1715
2346
|
startupHooks = [];
|
|
@@ -1720,6 +2351,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1720
2351
|
this.startupHooks.push(callback);
|
|
1721
2352
|
return this;
|
|
1722
2353
|
}
|
|
2354
|
+
specAvailableHooks = [];
|
|
2355
|
+
/**
|
|
2356
|
+
* Registers a callback to be executed when the OpenAPI spec is available.
|
|
2357
|
+
* This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
|
|
2358
|
+
*/
|
|
2359
|
+
onSpecAvailable(callback) {
|
|
2360
|
+
this.specAvailableHooks.push(callback);
|
|
2361
|
+
return this;
|
|
2362
|
+
}
|
|
1723
2363
|
/**
|
|
1724
2364
|
* Starts the application server.
|
|
1725
2365
|
*
|
|
@@ -1736,17 +2376,43 @@ class Shokupan extends ShokupanRouter {
|
|
|
1736
2376
|
}
|
|
1737
2377
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
1738
2378
|
this.openApiSpec = await generateOpenApi(this);
|
|
2379
|
+
for (const hook of this.specAvailableHooks) {
|
|
2380
|
+
await hook(this.openApiSpec);
|
|
2381
|
+
}
|
|
1739
2382
|
}
|
|
1740
2383
|
if (port === 0 && process.platform === "linux") ;
|
|
2384
|
+
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2385
|
+
this.cpuMonitor = new SystemCpuMonitor();
|
|
2386
|
+
this.cpuMonitor.start();
|
|
2387
|
+
}
|
|
1741
2388
|
const serveOptions = {
|
|
1742
2389
|
port: finalPort,
|
|
1743
2390
|
hostname: this.applicationConfig.hostname,
|
|
1744
2391
|
development: this.applicationConfig.development,
|
|
1745
2392
|
fetch: this.fetch.bind(this),
|
|
1746
2393
|
reusePort: this.applicationConfig.reusePort,
|
|
1747
|
-
idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0
|
|
2394
|
+
idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
|
|
2395
|
+
websocket: {
|
|
2396
|
+
open(ws) {
|
|
2397
|
+
ws.data?.handler?.open?.(ws);
|
|
2398
|
+
},
|
|
2399
|
+
message(ws, message) {
|
|
2400
|
+
ws.data?.handler?.message?.(ws, message);
|
|
2401
|
+
},
|
|
2402
|
+
drain(ws) {
|
|
2403
|
+
ws.data?.handler?.drain?.(ws);
|
|
2404
|
+
},
|
|
2405
|
+
close(ws, code, reason) {
|
|
2406
|
+
ws.data?.handler?.close?.(ws, code, reason);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
1748
2409
|
};
|
|
1749
|
-
|
|
2410
|
+
let factory = this.applicationConfig.serverFactory;
|
|
2411
|
+
if (!factory && typeof Bun === "undefined") {
|
|
2412
|
+
const { createHttpServer } = await import("./server-adapter-BWrEJbKL.js");
|
|
2413
|
+
factory = createHttpServer();
|
|
2414
|
+
}
|
|
2415
|
+
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
1750
2416
|
console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
|
|
1751
2417
|
return server;
|
|
1752
2418
|
}
|
|
@@ -1801,110 +2467,165 @@ class Shokupan extends ShokupanRouter {
|
|
|
1801
2467
|
* @returns The response to send.
|
|
1802
2468
|
*/
|
|
1803
2469
|
async fetch(req, server) {
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2470
|
+
if (this.applicationConfig.enableTracing) {
|
|
2471
|
+
const tracer2 = trace.getTracer("shokupan.application");
|
|
2472
|
+
const store = asyncContext.getStore();
|
|
2473
|
+
const attrs = {
|
|
2474
|
+
attributes: {
|
|
2475
|
+
"http.url": req.url,
|
|
2476
|
+
"http.method": req.method
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
const parent = store?.get("span");
|
|
2480
|
+
const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
|
|
2481
|
+
return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
|
|
2482
|
+
const ctxMap = /* @__PURE__ */ new Map();
|
|
2483
|
+
ctxMap.set("span", span);
|
|
2484
|
+
ctxMap.set("request", req);
|
|
2485
|
+
return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
if (this.applicationConfig.enableAsyncLocalStorage) {
|
|
1815
2489
|
const ctxMap = /* @__PURE__ */ new Map();
|
|
1816
|
-
ctxMap.set("span", span);
|
|
1817
2490
|
ctxMap.set("request", req);
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
if (result instanceof Response) {
|
|
1837
|
-
response = result;
|
|
1838
|
-
} else if (result === null || result === void 0) {
|
|
1839
|
-
span.setAttribute("http.status_code", 404);
|
|
1840
|
-
response = ctx2.text("Not Found", 404);
|
|
1841
|
-
} else if (typeof result === "object") {
|
|
1842
|
-
response = ctx2.json(result);
|
|
1843
|
-
} else {
|
|
1844
|
-
response = ctx2.text(String(result));
|
|
1845
|
-
}
|
|
1846
|
-
if (this.applicationConfig.hooks?.onRequestEnd) {
|
|
1847
|
-
await this.applicationConfig.hooks.onRequestEnd(ctx2);
|
|
1848
|
-
}
|
|
1849
|
-
if (this.applicationConfig.hooks?.onResponseStart) {
|
|
1850
|
-
await this.applicationConfig.hooks.onResponseStart(ctx2, response);
|
|
1851
|
-
}
|
|
1852
|
-
return response;
|
|
1853
|
-
} catch (err) {
|
|
1854
|
-
console.error(err);
|
|
1855
|
-
span.recordException(err);
|
|
1856
|
-
span.setStatus({ code: 2 });
|
|
1857
|
-
const status = err.status || err.statusCode || 500;
|
|
1858
|
-
const body = { error: err.message || "Internal Server Error" };
|
|
1859
|
-
if (err.errors) body.errors = err.errors;
|
|
1860
|
-
if (this.applicationConfig.hooks?.onError) {
|
|
1861
|
-
try {
|
|
1862
|
-
await this.applicationConfig.hooks.onError(err, ctx2);
|
|
1863
|
-
} catch (hookErr) {
|
|
1864
|
-
console.error("Error in onError hook:", hookErr);
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
return ctx2.json(body, status);
|
|
1868
|
-
}
|
|
1869
|
-
};
|
|
1870
|
-
let executionPromise = handle();
|
|
1871
|
-
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
1872
|
-
if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
|
|
1873
|
-
let timeoutId;
|
|
1874
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
1875
|
-
timeoutId = setTimeout(async () => {
|
|
1876
|
-
try {
|
|
1877
|
-
if (this.applicationConfig.hooks?.onRequestTimeout) {
|
|
1878
|
-
await this.applicationConfig.hooks.onRequestTimeout(ctx2);
|
|
1879
|
-
}
|
|
1880
|
-
} catch (e) {
|
|
1881
|
-
console.error("Error in onRequestTimeout hook:", e);
|
|
1882
|
-
}
|
|
1883
|
-
reject(new Error("Request Timeout"));
|
|
1884
|
-
}, timeoutMs);
|
|
1885
|
-
});
|
|
1886
|
-
executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
2491
|
+
return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
|
|
2492
|
+
}
|
|
2493
|
+
return this.handleRequest(req, server);
|
|
2494
|
+
}
|
|
2495
|
+
async handleRequest(req, server) {
|
|
2496
|
+
const request = req;
|
|
2497
|
+
const controller = new AbortController();
|
|
2498
|
+
const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
|
|
2499
|
+
const handle = async () => {
|
|
2500
|
+
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2501
|
+
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2502
|
+
const res = ctx.text(msg, 429);
|
|
2503
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2504
|
+
return res;
|
|
2505
|
+
}
|
|
2506
|
+
try {
|
|
2507
|
+
if (this.hasHook("onRequestStart")) {
|
|
2508
|
+
await this.executeHook("onRequestStart", ctx);
|
|
1887
2509
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2510
|
+
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2511
|
+
const result = await fn(ctx, async () => {
|
|
2512
|
+
const match = this.find(req.method, ctx.path);
|
|
2513
|
+
if (match) {
|
|
2514
|
+
ctx.params = match.params;
|
|
2515
|
+
return match.handler(ctx);
|
|
1891
2516
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2517
|
+
return null;
|
|
2518
|
+
});
|
|
2519
|
+
let response;
|
|
2520
|
+
if (result instanceof Response) {
|
|
2521
|
+
response = result;
|
|
2522
|
+
} else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
|
|
2523
|
+
response = ctx._finalResponse;
|
|
2524
|
+
} else if (result === null || result === void 0) {
|
|
2525
|
+
if (ctx._finalResponse instanceof Response) {
|
|
2526
|
+
response = ctx._finalResponse;
|
|
2527
|
+
} else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
|
|
2528
|
+
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2529
|
+
} else {
|
|
2530
|
+
response = ctx.text("Not Found", 404);
|
|
1897
2531
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2532
|
+
} else if (typeof result === "object") {
|
|
2533
|
+
response = ctx.json(result);
|
|
2534
|
+
} else {
|
|
2535
|
+
response = ctx.text(String(result));
|
|
2536
|
+
}
|
|
2537
|
+
if (this.hasHook("onRequestEnd")) {
|
|
2538
|
+
await this.executeHook("onRequestEnd", ctx);
|
|
2539
|
+
}
|
|
2540
|
+
if (this.hasHook("onResponseStart")) {
|
|
2541
|
+
await this.executeHook("onResponseStart", ctx, response);
|
|
2542
|
+
}
|
|
2543
|
+
return response;
|
|
2544
|
+
} catch (err) {
|
|
2545
|
+
console.error(err);
|
|
2546
|
+
const span = asyncContext.getStore()?.get("span");
|
|
2547
|
+
if (span) span.setStatus({ code: 2 });
|
|
2548
|
+
const status = err.status || err.statusCode || 500;
|
|
2549
|
+
const body = { error: err.message || "Internal Server Error" };
|
|
2550
|
+
if (err.errors) body.errors = err.errors;
|
|
2551
|
+
if (this.hasHook("onError")) {
|
|
2552
|
+
await this.executeHook("onError", err, ctx);
|
|
2553
|
+
}
|
|
2554
|
+
return ctx.json(body, status);
|
|
1905
2555
|
}
|
|
2556
|
+
};
|
|
2557
|
+
let executionPromise = handle();
|
|
2558
|
+
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
2559
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2560
|
+
let timeoutId;
|
|
2561
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2562
|
+
timeoutId = setTimeout(async () => {
|
|
2563
|
+
controller.abort();
|
|
2564
|
+
if (this.hasHook("onRequestTimeout")) {
|
|
2565
|
+
await this.executeHook("onRequestTimeout", ctx);
|
|
2566
|
+
}
|
|
2567
|
+
reject(new Error("Request Timeout"));
|
|
2568
|
+
}, timeoutMs);
|
|
2569
|
+
});
|
|
2570
|
+
executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
2571
|
+
}
|
|
2572
|
+
return executionPromise.catch((err) => {
|
|
2573
|
+
if (err.message === "Request Timeout") {
|
|
2574
|
+
return ctx.text("Request Timeout", 408);
|
|
2575
|
+
}
|
|
2576
|
+
console.error("Unexpected error in request execution:", err);
|
|
2577
|
+
return ctx.text("Internal Server Error", 500);
|
|
2578
|
+
}).then(async (res) => {
|
|
2579
|
+
if (this.hasHook("onResponseEnd")) {
|
|
2580
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2581
|
+
}
|
|
2582
|
+
return res;
|
|
1906
2583
|
});
|
|
1907
2584
|
}
|
|
2585
|
+
ensureHooksInitialized() {
|
|
2586
|
+
const hooks = this.applicationConfig.hooks;
|
|
2587
|
+
if (hooks) {
|
|
2588
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2589
|
+
const hookTypes = [
|
|
2590
|
+
"onRequestStart",
|
|
2591
|
+
"onRequestEnd",
|
|
2592
|
+
"onResponseStart",
|
|
2593
|
+
"onResponseEnd",
|
|
2594
|
+
"onError",
|
|
2595
|
+
"beforeValidate",
|
|
2596
|
+
"afterValidate",
|
|
2597
|
+
"onRequestTimeout",
|
|
2598
|
+
"onReadTimeout",
|
|
2599
|
+
"onWriteTimeout"
|
|
2600
|
+
];
|
|
2601
|
+
for (const type of hookTypes) {
|
|
2602
|
+
const fns = [];
|
|
2603
|
+
for (const h of hookList) {
|
|
2604
|
+
if (h[type]) fns.push(h[type]);
|
|
2605
|
+
}
|
|
2606
|
+
if (fns.length > 0) {
|
|
2607
|
+
this.hookCache.set(type, fns);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
this.hooksInitialized = true;
|
|
2612
|
+
}
|
|
2613
|
+
async executeHook(name, ...args) {
|
|
2614
|
+
if (!this.hooksInitialized) {
|
|
2615
|
+
this.ensureHooksInitialized();
|
|
2616
|
+
}
|
|
2617
|
+
const fns = this.hookCache.get(name);
|
|
2618
|
+
if (!fns) return;
|
|
2619
|
+
for (const fn of fns) {
|
|
2620
|
+
await fn(...args);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
hasHook(name) {
|
|
2624
|
+
if (!this.hooksInitialized) {
|
|
2625
|
+
this.ensureHooksInitialized();
|
|
2626
|
+
}
|
|
2627
|
+
return this.hookCache.has(name);
|
|
2628
|
+
}
|
|
1908
2629
|
}
|
|
1909
2630
|
class AuthPlugin extends ShokupanRouter {
|
|
1910
2631
|
constructor(authConfig) {
|
|
@@ -2109,7 +2830,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2109
2830
|
/**
|
|
2110
2831
|
* Middleware to verify JWT
|
|
2111
2832
|
*/
|
|
2112
|
-
|
|
2833
|
+
getMiddleware() {
|
|
2113
2834
|
return async (ctx, next) => {
|
|
2114
2835
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
2115
2836
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
@@ -2129,19 +2850,44 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2129
2850
|
}
|
|
2130
2851
|
}
|
|
2131
2852
|
function Compression(options = {}) {
|
|
2132
|
-
const threshold = options.threshold ??
|
|
2133
|
-
|
|
2853
|
+
const threshold = options.threshold ?? 512;
|
|
2854
|
+
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
2134
2855
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
2135
2856
|
let method = null;
|
|
2136
2857
|
if (acceptEncoding.includes("br")) method = "br";
|
|
2137
|
-
else if (acceptEncoding.includes("
|
|
2858
|
+
else if (acceptEncoding.includes("zstd")) {
|
|
2859
|
+
if (typeof Bun === "undefined") {
|
|
2860
|
+
throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
|
|
2861
|
+
}
|
|
2862
|
+
method = "zstd";
|
|
2863
|
+
} else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
2138
2864
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
2139
2865
|
if (!method) return next();
|
|
2140
|
-
|
|
2866
|
+
let response = await next();
|
|
2867
|
+
if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
|
|
2868
|
+
response = ctx._finalResponse;
|
|
2869
|
+
}
|
|
2141
2870
|
if (response instanceof Response) {
|
|
2142
2871
|
if (response.headers.has("Content-Encoding")) return response;
|
|
2143
|
-
|
|
2144
|
-
|
|
2872
|
+
let body;
|
|
2873
|
+
let bodySize;
|
|
2874
|
+
if (ctx._rawBody !== void 0) {
|
|
2875
|
+
if (typeof ctx._rawBody === "string") {
|
|
2876
|
+
const encoded = new TextEncoder().encode(ctx._rawBody);
|
|
2877
|
+
body = encoded.buffer;
|
|
2878
|
+
bodySize = encoded.byteLength;
|
|
2879
|
+
} else if (ctx._rawBody instanceof Uint8Array) {
|
|
2880
|
+
body = ctx._rawBody.buffer;
|
|
2881
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2882
|
+
} else {
|
|
2883
|
+
body = ctx._rawBody;
|
|
2884
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2885
|
+
}
|
|
2886
|
+
} else {
|
|
2887
|
+
body = await response.arrayBuffer();
|
|
2888
|
+
bodySize = body.byteLength;
|
|
2889
|
+
}
|
|
2890
|
+
if (bodySize < threshold) {
|
|
2145
2891
|
return new Response(body, {
|
|
2146
2892
|
status: response.status,
|
|
2147
2893
|
statusText: response.statusText,
|
|
@@ -2149,17 +2895,36 @@ function Compression(options = {}) {
|
|
|
2149
2895
|
});
|
|
2150
2896
|
}
|
|
2151
2897
|
let compressed;
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2898
|
+
switch (method) {
|
|
2899
|
+
case "br":
|
|
2900
|
+
compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
|
|
2901
|
+
params: {
|
|
2902
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
2903
|
+
}
|
|
2904
|
+
}, (err, data) => {
|
|
2905
|
+
if (err) return rej(err);
|
|
2906
|
+
res(data);
|
|
2907
|
+
}));
|
|
2908
|
+
break;
|
|
2909
|
+
case "gzip":
|
|
2910
|
+
compressed = await new Promise((res, rej) => zlib.gzip(body, (err, data) => {
|
|
2911
|
+
if (err) return rej(err);
|
|
2912
|
+
res(data);
|
|
2913
|
+
}));
|
|
2914
|
+
break;
|
|
2915
|
+
case "zstd":
|
|
2916
|
+
compressed = await Bun.zstdCompress(body);
|
|
2917
|
+
break;
|
|
2918
|
+
default:
|
|
2919
|
+
compressed = await new Promise((res, rej) => zlib.deflate(body, (err, data) => {
|
|
2920
|
+
if (err) return rej(err);
|
|
2921
|
+
res(data);
|
|
2922
|
+
}));
|
|
2923
|
+
break;
|
|
2158
2924
|
}
|
|
2159
2925
|
const headers = new Headers(response.headers);
|
|
2160
2926
|
headers.set("Content-Encoding", method);
|
|
2161
2927
|
headers.set("Content-Length", String(compressed.length));
|
|
2162
|
-
headers.delete("Content-Length");
|
|
2163
2928
|
return new Response(compressed, {
|
|
2164
2929
|
status: response.status,
|
|
2165
2930
|
statusText: response.statusText,
|
|
@@ -2168,6 +2933,9 @@ function Compression(options = {}) {
|
|
|
2168
2933
|
}
|
|
2169
2934
|
return response;
|
|
2170
2935
|
};
|
|
2936
|
+
compressionMiddleware.isBuiltin = true;
|
|
2937
|
+
compressionMiddleware.pluginName = "Compression";
|
|
2938
|
+
return compressionMiddleware;
|
|
2171
2939
|
}
|
|
2172
2940
|
function Cors(options = {}) {
|
|
2173
2941
|
const defaults2 = {
|
|
@@ -2177,7 +2945,7 @@ function Cors(options = {}) {
|
|
|
2177
2945
|
optionsSuccessStatus: 204
|
|
2178
2946
|
};
|
|
2179
2947
|
const opts = { ...defaults2, ...options };
|
|
2180
|
-
|
|
2948
|
+
const corsMiddleware = async function CorsMiddleware(ctx, next) {
|
|
2181
2949
|
const headers = new Headers();
|
|
2182
2950
|
const origin = ctx.headers.get("origin");
|
|
2183
2951
|
const set = (k, v) => headers.set(k, v);
|
|
@@ -2239,6 +3007,9 @@ function Cors(options = {}) {
|
|
|
2239
3007
|
}
|
|
2240
3008
|
return response;
|
|
2241
3009
|
};
|
|
3010
|
+
corsMiddleware.isBuiltin = true;
|
|
3011
|
+
corsMiddleware.pluginName = "Cors";
|
|
3012
|
+
return corsMiddleware;
|
|
2242
3013
|
}
|
|
2243
3014
|
function useExpress(expressMiddleware) {
|
|
2244
3015
|
return async (ctx, next) => {
|
|
@@ -2300,122 +3071,409 @@ function useExpress(expressMiddleware) {
|
|
|
2300
3071
|
});
|
|
2301
3072
|
};
|
|
2302
3073
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
2310
|
-
return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
|
|
2311
|
-
});
|
|
2312
|
-
const skip = options.skip || (() => false);
|
|
2313
|
-
const hits = /* @__PURE__ */ new Map();
|
|
2314
|
-
const interval = setInterval(() => {
|
|
2315
|
-
const now = Date.now();
|
|
2316
|
-
for (const [key, record] of hits.entries()) {
|
|
2317
|
-
if (record.resetTime <= now) {
|
|
2318
|
-
hits.delete(key);
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
}, windowMs);
|
|
2322
|
-
if (interval.unref) interval.unref();
|
|
2323
|
-
return async (ctx, next) => {
|
|
2324
|
-
if (skip(ctx)) return next();
|
|
2325
|
-
const key = keyGenerator(ctx);
|
|
2326
|
-
const now = Date.now();
|
|
2327
|
-
let record = hits.get(key);
|
|
2328
|
-
if (!record || record.resetTime <= now) {
|
|
2329
|
-
record = {
|
|
2330
|
-
hits: 0,
|
|
2331
|
-
resetTime: now + windowMs
|
|
2332
|
-
};
|
|
2333
|
-
hits.set(key, record);
|
|
2334
|
-
}
|
|
2335
|
-
record.hits++;
|
|
2336
|
-
const remaining = Math.max(0, max - record.hits);
|
|
2337
|
-
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
2338
|
-
if (record.hits > max) {
|
|
2339
|
-
if (headers) {
|
|
2340
|
-
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2341
|
-
res.headers.set("X-RateLimit-Limit", String(max));
|
|
2342
|
-
res.headers.set("X-RateLimit-Remaining", "0");
|
|
2343
|
-
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2344
|
-
return res;
|
|
2345
|
-
}
|
|
2346
|
-
return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2347
|
-
}
|
|
2348
|
-
const response = await next();
|
|
2349
|
-
if (response instanceof Response && headers) {
|
|
2350
|
-
response.headers.set("X-RateLimit-Limit", String(max));
|
|
2351
|
-
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
2352
|
-
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2353
|
-
}
|
|
2354
|
-
return response;
|
|
2355
|
-
};
|
|
3074
|
+
class ValidationError extends Error {
|
|
3075
|
+
constructor(errors) {
|
|
3076
|
+
super("Validation Error");
|
|
3077
|
+
this.errors = errors;
|
|
3078
|
+
}
|
|
3079
|
+
status = 400;
|
|
2356
3080
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
3081
|
+
function isZod(schema) {
|
|
3082
|
+
return typeof schema?.safeParse === "function";
|
|
3083
|
+
}
|
|
3084
|
+
async function validateZod(schema, data) {
|
|
3085
|
+
const result = await schema.safeParseAsync(data);
|
|
3086
|
+
if (!result.success) {
|
|
3087
|
+
throw new ValidationError(result.error.errors);
|
|
2363
3088
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
<meta charset = "utf-8" />
|
|
2373
|
-
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
2374
|
-
</head>
|
|
2375
|
-
|
|
2376
|
-
<body>
|
|
2377
|
-
<div id="app"></div>
|
|
2378
|
-
|
|
2379
|
-
<script src="<%= it.path %>scalar.js"><\/script>
|
|
2380
|
-
<script>
|
|
2381
|
-
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
2382
|
-
url: "<%= it.path %>openapi.json",
|
|
2383
|
-
}
|
|
2384
|
-
])
|
|
2385
|
-
<\/script>
|
|
2386
|
-
</body>
|
|
2387
|
-
|
|
2388
|
-
</html>`, { path, config: this.pluginOptions }));
|
|
2389
|
-
});
|
|
2390
|
-
this.get("/scalar.js", (ctx) => {
|
|
2391
|
-
return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
|
|
2392
|
-
});
|
|
2393
|
-
this.get("/openapi.json", async (ctx) => {
|
|
2394
|
-
let spec;
|
|
2395
|
-
if (this.root.openApiSpec) {
|
|
2396
|
-
try {
|
|
2397
|
-
spec = structuredClone(this.root.openApiSpec);
|
|
2398
|
-
} catch (e) {
|
|
2399
|
-
spec = Object.assign({}, this.root.openApiSpec);
|
|
2400
|
-
}
|
|
2401
|
-
} else {
|
|
2402
|
-
spec = await (this.root || this).generateApiSpec();
|
|
2403
|
-
}
|
|
2404
|
-
if (this.pluginOptions.baseDocument) {
|
|
2405
|
-
deepMerge(spec, this.pluginOptions.baseDocument);
|
|
2406
|
-
}
|
|
2407
|
-
return ctx.json(spec);
|
|
2408
|
-
});
|
|
3089
|
+
return result.data;
|
|
3090
|
+
}
|
|
3091
|
+
function isTypeBox(schema) {
|
|
3092
|
+
return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
|
|
3093
|
+
}
|
|
3094
|
+
function validateTypeBox(schema, data) {
|
|
3095
|
+
if (!schema.Check(data)) {
|
|
3096
|
+
throw new ValidationError([...schema.Errors(data)]);
|
|
2409
3097
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
3098
|
+
return data;
|
|
3099
|
+
}
|
|
3100
|
+
function isAjv(schema) {
|
|
3101
|
+
return typeof schema === "function" && "errors" in schema;
|
|
3102
|
+
}
|
|
3103
|
+
function validateAjv(schema, data) {
|
|
3104
|
+
const valid = schema(data);
|
|
3105
|
+
if (!valid) {
|
|
3106
|
+
throw new ValidationError(schema.errors);
|
|
3107
|
+
}
|
|
3108
|
+
return data;
|
|
3109
|
+
}
|
|
3110
|
+
const valibot = (schema, parser) => {
|
|
3111
|
+
return {
|
|
3112
|
+
_valibot: true,
|
|
3113
|
+
schema,
|
|
3114
|
+
parser
|
|
3115
|
+
};
|
|
3116
|
+
};
|
|
3117
|
+
function isValibotWrapper(schema) {
|
|
3118
|
+
return schema?._valibot === true;
|
|
3119
|
+
}
|
|
3120
|
+
async function validateValibotWrapper(wrapper, data) {
|
|
3121
|
+
const result = await wrapper.parser(wrapper.schema, data);
|
|
3122
|
+
if (!result.success) {
|
|
3123
|
+
throw new ValidationError(result.issues);
|
|
3124
|
+
}
|
|
3125
|
+
return result.output;
|
|
3126
|
+
}
|
|
3127
|
+
function isClass(schema) {
|
|
3128
|
+
try {
|
|
3129
|
+
if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
|
|
3130
|
+
return true;
|
|
3131
|
+
}
|
|
3132
|
+
return typeof schema === "function" && schema.prototype && schema.name;
|
|
3133
|
+
} catch {
|
|
3134
|
+
return false;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
async function validateClassValidator(schema, data) {
|
|
3138
|
+
const object = plainToInstance(schema, data);
|
|
3139
|
+
try {
|
|
3140
|
+
await validateOrReject(object);
|
|
3141
|
+
return object;
|
|
3142
|
+
} catch (errors) {
|
|
3143
|
+
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
3144
|
+
property: err.property,
|
|
3145
|
+
constraints: err.constraints,
|
|
3146
|
+
children: err.children
|
|
3147
|
+
})) : errors;
|
|
3148
|
+
throw new ValidationError(formattedErrors);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
const safelyGetBody = async (ctx) => {
|
|
3152
|
+
const req = ctx.req;
|
|
3153
|
+
if (req._bodyParsed) {
|
|
3154
|
+
return req._bodyValue;
|
|
3155
|
+
}
|
|
3156
|
+
try {
|
|
3157
|
+
let data;
|
|
3158
|
+
if (typeof req.json === "function") {
|
|
3159
|
+
data = await req.json();
|
|
3160
|
+
} else {
|
|
3161
|
+
data = req.body;
|
|
3162
|
+
if (typeof data === "string") {
|
|
3163
|
+
try {
|
|
3164
|
+
data = JSON.parse(data);
|
|
3165
|
+
} catch {
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
req._bodyParsed = true;
|
|
3170
|
+
req._bodyValue = data;
|
|
3171
|
+
Object.defineProperty(req, "json", {
|
|
3172
|
+
value: async () => req._bodyValue,
|
|
3173
|
+
configurable: true
|
|
3174
|
+
});
|
|
3175
|
+
return data;
|
|
3176
|
+
} catch (e) {
|
|
3177
|
+
return {};
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
function getValidator(schema) {
|
|
3181
|
+
if (isZod(schema)) {
|
|
3182
|
+
return (data) => validateZod(schema, data);
|
|
3183
|
+
}
|
|
3184
|
+
if (isTypeBox(schema)) {
|
|
3185
|
+
return (data) => validateTypeBox(schema, data);
|
|
3186
|
+
}
|
|
3187
|
+
if (isAjv(schema)) {
|
|
3188
|
+
return (data) => validateAjv(schema, data);
|
|
3189
|
+
}
|
|
3190
|
+
if (isValibotWrapper(schema)) {
|
|
3191
|
+
return (data) => validateValibotWrapper(schema, data);
|
|
3192
|
+
}
|
|
3193
|
+
if (isClass(schema)) {
|
|
3194
|
+
return (data) => validateClassValidator(schema, data);
|
|
3195
|
+
}
|
|
3196
|
+
if (typeof schema === "function") {
|
|
3197
|
+
return schema;
|
|
3198
|
+
}
|
|
3199
|
+
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
3200
|
+
}
|
|
3201
|
+
function validate(config) {
|
|
3202
|
+
const validators = {};
|
|
3203
|
+
if (config.params) validators.params = getValidator(config.params);
|
|
3204
|
+
if (config.query) validators.query = getValidator(config.query);
|
|
3205
|
+
if (config.headers) validators.headers = getValidator(config.headers);
|
|
3206
|
+
if (config.body) validators.body = getValidator(config.body);
|
|
3207
|
+
return async (ctx, next) => {
|
|
3208
|
+
const dataToValidate = {};
|
|
3209
|
+
if (config.params) dataToValidate.params = ctx.params;
|
|
3210
|
+
let queryObj;
|
|
3211
|
+
if (config.query) {
|
|
3212
|
+
const url = new URL(ctx.req.url);
|
|
3213
|
+
queryObj = Object.fromEntries(url.searchParams.entries());
|
|
3214
|
+
dataToValidate.query = queryObj;
|
|
3215
|
+
}
|
|
3216
|
+
if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
|
|
3217
|
+
let body;
|
|
3218
|
+
if (config.body) {
|
|
3219
|
+
body = await safelyGetBody(ctx);
|
|
3220
|
+
dataToValidate.body = body;
|
|
3221
|
+
}
|
|
3222
|
+
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
3223
|
+
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
3224
|
+
}
|
|
3225
|
+
if (validators.params) {
|
|
3226
|
+
ctx.params = await validators.params(ctx.params);
|
|
3227
|
+
}
|
|
3228
|
+
let validQuery;
|
|
3229
|
+
if (validators.query && queryObj) {
|
|
3230
|
+
validQuery = await validators.query(queryObj);
|
|
3231
|
+
}
|
|
3232
|
+
if (validators.headers) {
|
|
3233
|
+
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
3234
|
+
await validators.headers(headersObj);
|
|
3235
|
+
}
|
|
3236
|
+
let validBody;
|
|
3237
|
+
if (validators.body) {
|
|
3238
|
+
const b = body ?? await safelyGetBody(ctx);
|
|
3239
|
+
validBody = await validators.body(b);
|
|
3240
|
+
const req = ctx.req;
|
|
3241
|
+
req._bodyValue = validBody;
|
|
3242
|
+
Object.defineProperty(req, "json", {
|
|
3243
|
+
value: async () => validBody,
|
|
3244
|
+
configurable: true
|
|
3245
|
+
});
|
|
3246
|
+
ctx.body = validBody;
|
|
3247
|
+
}
|
|
3248
|
+
if (ctx.app?.applicationConfig.hooks?.afterValidate) {
|
|
3249
|
+
const validatedData = { ...dataToValidate };
|
|
3250
|
+
if (config.params) validatedData.params = ctx.params;
|
|
3251
|
+
if (config.query) validatedData.query = validQuery;
|
|
3252
|
+
if (config.body) validatedData.body = validBody;
|
|
3253
|
+
await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
|
|
3254
|
+
}
|
|
3255
|
+
return next();
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
const ajv = new Ajv({ coerceTypes: true, allErrors: true });
|
|
3259
|
+
addFormats(ajv);
|
|
3260
|
+
const compiledValidators = /* @__PURE__ */ new WeakMap();
|
|
3261
|
+
function openApiValidator() {
|
|
3262
|
+
return async (ctx, next) => {
|
|
3263
|
+
const app = ctx.app;
|
|
3264
|
+
if (!app || !app.openApiSpec) {
|
|
3265
|
+
return next();
|
|
3266
|
+
}
|
|
3267
|
+
let cache = compiledValidators.get(app);
|
|
3268
|
+
if (!cache) {
|
|
3269
|
+
cache = compileValidators(app.openApiSpec);
|
|
3270
|
+
compiledValidators.set(app, cache);
|
|
3271
|
+
}
|
|
3272
|
+
let matchPath;
|
|
3273
|
+
let matchParams = {};
|
|
3274
|
+
if (cache.validators.has(ctx.path)) {
|
|
3275
|
+
matchPath = ctx.path;
|
|
3276
|
+
} else {
|
|
3277
|
+
for (const [path, { regex, paramNames }] of cache.paths) {
|
|
3278
|
+
const match = regex.exec(ctx.path);
|
|
3279
|
+
if (match) {
|
|
3280
|
+
matchPath = path;
|
|
3281
|
+
paramNames.forEach((name, i) => {
|
|
3282
|
+
matchParams[name] = match[i + 1];
|
|
3283
|
+
});
|
|
3284
|
+
break;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
if (!matchPath) {
|
|
3289
|
+
return next();
|
|
3290
|
+
}
|
|
3291
|
+
const method = ctx.req.method.toLowerCase();
|
|
3292
|
+
const validators = cache.validators.get(matchPath)?.[method];
|
|
3293
|
+
if (!validators) {
|
|
3294
|
+
return next();
|
|
3295
|
+
}
|
|
3296
|
+
const errors = [];
|
|
3297
|
+
if (validators.body) {
|
|
3298
|
+
let body;
|
|
3299
|
+
try {
|
|
3300
|
+
body = await ctx.req.json().catch(() => ({}));
|
|
3301
|
+
} catch {
|
|
3302
|
+
body = {};
|
|
3303
|
+
}
|
|
3304
|
+
const valid = validators.body(body);
|
|
3305
|
+
if (!valid && validators.body.errors) {
|
|
3306
|
+
errors.push(...validators.body.errors.map((e) => ({ ...e, location: "body" })));
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
if (validators.query) {
|
|
3310
|
+
const query = Object.fromEntries(new URL(ctx.req.url).searchParams.entries());
|
|
3311
|
+
const valid = validators.query(query);
|
|
3312
|
+
if (!valid && validators.query.errors) {
|
|
3313
|
+
errors.push(...validators.query.errors.map((e) => ({ ...e, location: "query" })));
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
if (validators.params) {
|
|
3317
|
+
const params = { ...matchParams, ...ctx.params };
|
|
3318
|
+
const valid = validators.params(params);
|
|
3319
|
+
if (!valid && validators.params.errors) {
|
|
3320
|
+
errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
if (validators.headers) {
|
|
3324
|
+
const headers = Object.fromEntries(ctx.req.headers.entries());
|
|
3325
|
+
const valid = validators.headers(headers);
|
|
3326
|
+
if (!valid && validators.headers.errors) {
|
|
3327
|
+
errors.push(...validators.headers.errors.map((e) => ({ ...e, location: "header" })));
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
if (errors.length > 0) {
|
|
3331
|
+
throw new ValidationError(errors);
|
|
3332
|
+
}
|
|
3333
|
+
return next();
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
function compileValidators(spec) {
|
|
3337
|
+
const validators = /* @__PURE__ */ new Map();
|
|
3338
|
+
const paths = /* @__PURE__ */ new Map();
|
|
3339
|
+
for (const [path, pathItem] of Object.entries(spec.paths || {})) {
|
|
3340
|
+
if (path.includes("{")) {
|
|
3341
|
+
const paramNames = [];
|
|
3342
|
+
const regexStr = "^" + path.replace(/{([^}]+)}/g, (_, name) => {
|
|
3343
|
+
paramNames.push(name);
|
|
3344
|
+
return "([^/]+)";
|
|
3345
|
+
}) + "$";
|
|
3346
|
+
paths.set(path, {
|
|
3347
|
+
regex: new RegExp(regexStr),
|
|
3348
|
+
paramNames
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
const pathValidators = {};
|
|
3352
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
3353
|
+
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
3354
|
+
const oper = operation;
|
|
3355
|
+
const opValidators = {};
|
|
3356
|
+
if (oper.requestBody?.content?.["application/json"]?.schema) {
|
|
3357
|
+
opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
|
|
3358
|
+
}
|
|
3359
|
+
const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
|
|
3360
|
+
const queryProps = {};
|
|
3361
|
+
const pathProps = {};
|
|
3362
|
+
const headerProps = {};
|
|
3363
|
+
const queryRequired = [];
|
|
3364
|
+
const pathRequired = [];
|
|
3365
|
+
const headerRequired = [];
|
|
3366
|
+
for (const param of parameters) {
|
|
3367
|
+
if (param.in === "query") {
|
|
3368
|
+
queryProps[param.name] = param.schema || {};
|
|
3369
|
+
if (param.required) queryRequired.push(param.name);
|
|
3370
|
+
} else if (param.in === "path") {
|
|
3371
|
+
pathProps[param.name] = param.schema || {};
|
|
3372
|
+
pathRequired.push(param.name);
|
|
3373
|
+
} else if (param.in === "header") {
|
|
3374
|
+
headerProps[param.name] = param.schema || {};
|
|
3375
|
+
if (param.required) headerRequired.push(param.name);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
if (Object.keys(queryProps).length > 0) {
|
|
3379
|
+
opValidators.query = ajv.compile({
|
|
3380
|
+
type: "object",
|
|
3381
|
+
properties: queryProps,
|
|
3382
|
+
required: queryRequired.length > 0 ? queryRequired : void 0
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
3385
|
+
if (Object.keys(pathProps).length > 0) {
|
|
3386
|
+
opValidators.params = ajv.compile({
|
|
3387
|
+
type: "object",
|
|
3388
|
+
properties: pathProps,
|
|
3389
|
+
required: pathRequired.length > 0 ? pathRequired : void 0
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
if (Object.keys(headerProps).length > 0) {
|
|
3393
|
+
opValidators.headers = ajv.compile({
|
|
3394
|
+
type: "object",
|
|
3395
|
+
properties: headerProps,
|
|
3396
|
+
required: headerRequired.length > 0 ? headerRequired : void 0
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
3399
|
+
pathValidators[method] = opValidators;
|
|
3400
|
+
}
|
|
3401
|
+
validators.set(path, pathValidators);
|
|
3402
|
+
}
|
|
3403
|
+
return { paths, validators };
|
|
3404
|
+
}
|
|
3405
|
+
function precompileValidators(app, spec) {
|
|
3406
|
+
const cache = compileValidators(spec);
|
|
3407
|
+
compiledValidators.set(app, cache);
|
|
3408
|
+
}
|
|
3409
|
+
function enableOpenApiValidation(app) {
|
|
3410
|
+
app.use(openApiValidator());
|
|
3411
|
+
app.onSpecAvailable((spec) => {
|
|
3412
|
+
precompileValidators(app, spec);
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
const eta = new Eta();
|
|
3416
|
+
class ScalarPlugin extends ShokupanRouter {
|
|
3417
|
+
constructor(pluginOptions) {
|
|
3418
|
+
super();
|
|
3419
|
+
this.pluginOptions = pluginOptions;
|
|
3420
|
+
this.init();
|
|
3421
|
+
}
|
|
3422
|
+
init() {
|
|
3423
|
+
this.get("/", (ctx) => {
|
|
3424
|
+
let path = ctx.url.toString();
|
|
3425
|
+
if (!path.endsWith("/")) path += "/";
|
|
3426
|
+
return ctx.html(eta.renderString(`<!doctype html>
|
|
3427
|
+
<html>
|
|
3428
|
+
<head>
|
|
3429
|
+
<title>API Reference</title>
|
|
3430
|
+
<meta charset = "utf-8" />
|
|
3431
|
+
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
3432
|
+
</head>
|
|
3433
|
+
|
|
3434
|
+
<body>
|
|
3435
|
+
<div id="app"></div>
|
|
3436
|
+
|
|
3437
|
+
<script src="<%= it.path %>scalar.js"><\/script>
|
|
3438
|
+
<script>
|
|
3439
|
+
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
3440
|
+
url: "<%= it.path %>openapi.json",
|
|
3441
|
+
}
|
|
3442
|
+
])
|
|
3443
|
+
<\/script>
|
|
3444
|
+
</body>
|
|
3445
|
+
|
|
3446
|
+
</html>`, { path, config: this.pluginOptions }));
|
|
3447
|
+
});
|
|
3448
|
+
this.get("/scalar.js", (ctx) => {
|
|
3449
|
+
return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
|
|
3450
|
+
});
|
|
3451
|
+
this.get("/openapi.json", async (ctx) => {
|
|
3452
|
+
let spec;
|
|
3453
|
+
if (this.root.openApiSpec) {
|
|
3454
|
+
try {
|
|
3455
|
+
spec = structuredClone(this.root.openApiSpec);
|
|
3456
|
+
} catch (e) {
|
|
3457
|
+
spec = Object.assign({}, this.root.openApiSpec);
|
|
3458
|
+
}
|
|
3459
|
+
} else {
|
|
3460
|
+
spec = await (this.root || this).generateApiSpec();
|
|
3461
|
+
}
|
|
3462
|
+
if (this.pluginOptions.baseDocument) {
|
|
3463
|
+
deepMerge(spec, this.pluginOptions.baseDocument);
|
|
3464
|
+
}
|
|
3465
|
+
return ctx.json(spec);
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
// New lifecycle method to be called by router.mount
|
|
3469
|
+
onMount(parent) {
|
|
3470
|
+
if (parent.onStart) {
|
|
3471
|
+
parent.onStart(async () => {
|
|
3472
|
+
if (this.pluginOptions.enableStaticAnalysis) {
|
|
3473
|
+
try {
|
|
3474
|
+
const entrypoint = process.argv[1];
|
|
3475
|
+
console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
|
|
3476
|
+
const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
2419
3477
|
let staticSpec = await analyzer.analyze();
|
|
2420
3478
|
if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
|
|
2421
3479
|
deepMerge(this.pluginOptions.baseDocument, staticSpec);
|
|
@@ -2429,7 +3487,7 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
2429
3487
|
}
|
|
2430
3488
|
}
|
|
2431
3489
|
function SecurityHeaders(options = {}) {
|
|
2432
|
-
|
|
3490
|
+
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
2433
3491
|
const headers = {};
|
|
2434
3492
|
const set = (k, v) => headers[k] = v;
|
|
2435
3493
|
if (options.dnsPrefetchControl !== false) {
|
|
@@ -2483,6 +3541,9 @@ function SecurityHeaders(options = {}) {
|
|
|
2483
3541
|
}
|
|
2484
3542
|
return response;
|
|
2485
3543
|
};
|
|
3544
|
+
securityHeadersMiddleware.isBuiltin = true;
|
|
3545
|
+
securityHeadersMiddleware.pluginName = "SecurityHeaders";
|
|
3546
|
+
return securityHeadersMiddleware;
|
|
2486
3547
|
}
|
|
2487
3548
|
class Cookie {
|
|
2488
3549
|
maxAge;
|
|
@@ -2596,7 +3657,7 @@ function Session(options) {
|
|
|
2596
3657
|
const resave = options.resave === void 0 ? true : options.resave;
|
|
2597
3658
|
const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
|
|
2598
3659
|
const rolling = options.rolling || false;
|
|
2599
|
-
|
|
3660
|
+
const sessionMiddleware = async function SessionMiddleware(ctx, next) {
|
|
2600
3661
|
let reqSessionId = null;
|
|
2601
3662
|
const cookieHeader = ctx.req.headers.get("cookie");
|
|
2602
3663
|
const cookies = {};
|
|
@@ -2732,194 +3793,9 @@ function Session(options) {
|
|
|
2732
3793
|
}
|
|
2733
3794
|
return result;
|
|
2734
3795
|
};
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
super("Validation Error");
|
|
2739
|
-
this.errors = errors;
|
|
2740
|
-
}
|
|
2741
|
-
status = 400;
|
|
2742
|
-
}
|
|
2743
|
-
function isZod(schema) {
|
|
2744
|
-
return typeof schema?.safeParse === "function";
|
|
2745
|
-
}
|
|
2746
|
-
async function validateZod(schema, data) {
|
|
2747
|
-
const result = await schema.safeParseAsync(data);
|
|
2748
|
-
if (!result.success) {
|
|
2749
|
-
throw new ValidationError(result.error.errors);
|
|
2750
|
-
}
|
|
2751
|
-
return result.data;
|
|
2752
|
-
}
|
|
2753
|
-
function isTypeBox(schema) {
|
|
2754
|
-
return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
|
|
2755
|
-
}
|
|
2756
|
-
function validateTypeBox(schema, data) {
|
|
2757
|
-
if (!schema.Check(data)) {
|
|
2758
|
-
throw new ValidationError([...schema.Errors(data)]);
|
|
2759
|
-
}
|
|
2760
|
-
return data;
|
|
2761
|
-
}
|
|
2762
|
-
function isAjv(schema) {
|
|
2763
|
-
return typeof schema === "function" && "errors" in schema;
|
|
2764
|
-
}
|
|
2765
|
-
function validateAjv(schema, data) {
|
|
2766
|
-
const valid = schema(data);
|
|
2767
|
-
if (!valid) {
|
|
2768
|
-
throw new ValidationError(schema.errors);
|
|
2769
|
-
}
|
|
2770
|
-
return data;
|
|
2771
|
-
}
|
|
2772
|
-
const valibot = (schema, parser) => {
|
|
2773
|
-
return {
|
|
2774
|
-
_valibot: true,
|
|
2775
|
-
schema,
|
|
2776
|
-
parser
|
|
2777
|
-
};
|
|
2778
|
-
};
|
|
2779
|
-
function isValibotWrapper(schema) {
|
|
2780
|
-
return schema?._valibot === true;
|
|
2781
|
-
}
|
|
2782
|
-
async function validateValibotWrapper(wrapper, data) {
|
|
2783
|
-
const result = await wrapper.parser(wrapper.schema, data);
|
|
2784
|
-
if (!result.success) {
|
|
2785
|
-
throw new ValidationError(result.issues);
|
|
2786
|
-
}
|
|
2787
|
-
return result.output;
|
|
2788
|
-
}
|
|
2789
|
-
function isClass(schema) {
|
|
2790
|
-
try {
|
|
2791
|
-
if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
|
|
2792
|
-
return true;
|
|
2793
|
-
}
|
|
2794
|
-
return typeof schema === "function" && schema.prototype && schema.name;
|
|
2795
|
-
} catch {
|
|
2796
|
-
return false;
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
async function validateClassValidator(schema, data) {
|
|
2800
|
-
const object = plainToInstance(schema, data);
|
|
2801
|
-
try {
|
|
2802
|
-
await validateOrReject(object);
|
|
2803
|
-
return object;
|
|
2804
|
-
} catch (errors) {
|
|
2805
|
-
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
2806
|
-
property: err.property,
|
|
2807
|
-
constraints: err.constraints,
|
|
2808
|
-
children: err.children
|
|
2809
|
-
})) : errors;
|
|
2810
|
-
throw new ValidationError(formattedErrors);
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
|
-
const safelyGetBody = async (ctx) => {
|
|
2814
|
-
const req = ctx.req;
|
|
2815
|
-
if (req._bodyParsed) {
|
|
2816
|
-
return req._bodyValue;
|
|
2817
|
-
}
|
|
2818
|
-
try {
|
|
2819
|
-
let data;
|
|
2820
|
-
if (typeof req.json === "function") {
|
|
2821
|
-
data = await req.json();
|
|
2822
|
-
} else {
|
|
2823
|
-
data = req.body;
|
|
2824
|
-
if (typeof data === "string") {
|
|
2825
|
-
try {
|
|
2826
|
-
data = JSON.parse(data);
|
|
2827
|
-
} catch {
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
}
|
|
2831
|
-
req._bodyParsed = true;
|
|
2832
|
-
req._bodyValue = data;
|
|
2833
|
-
Object.defineProperty(req, "json", {
|
|
2834
|
-
value: async () => req._bodyValue,
|
|
2835
|
-
configurable: true
|
|
2836
|
-
});
|
|
2837
|
-
return data;
|
|
2838
|
-
} catch (e) {
|
|
2839
|
-
return {};
|
|
2840
|
-
}
|
|
2841
|
-
};
|
|
2842
|
-
function validate(config) {
|
|
2843
|
-
return async (ctx, next) => {
|
|
2844
|
-
const dataToValidate = {};
|
|
2845
|
-
if (config.params) dataToValidate.params = ctx.params;
|
|
2846
|
-
let queryObj;
|
|
2847
|
-
if (config.query) {
|
|
2848
|
-
const url = new URL(ctx.req.url);
|
|
2849
|
-
queryObj = Object.fromEntries(url.searchParams.entries());
|
|
2850
|
-
dataToValidate.query = queryObj;
|
|
2851
|
-
}
|
|
2852
|
-
if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
|
|
2853
|
-
let body;
|
|
2854
|
-
if (config.body) {
|
|
2855
|
-
body = await safelyGetBody(ctx);
|
|
2856
|
-
dataToValidate.body = body;
|
|
2857
|
-
}
|
|
2858
|
-
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
2859
|
-
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
2860
|
-
}
|
|
2861
|
-
if (config.params) {
|
|
2862
|
-
ctx.params = await runValidation(config.params, ctx.params);
|
|
2863
|
-
}
|
|
2864
|
-
let validQuery;
|
|
2865
|
-
if (config.query && queryObj) {
|
|
2866
|
-
validQuery = await runValidation(config.query, queryObj);
|
|
2867
|
-
}
|
|
2868
|
-
if (config.headers) {
|
|
2869
|
-
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2870
|
-
await runValidation(config.headers, headersObj);
|
|
2871
|
-
}
|
|
2872
|
-
let validBody;
|
|
2873
|
-
if (config.body) {
|
|
2874
|
-
const b = body ?? await safelyGetBody(ctx);
|
|
2875
|
-
validBody = await runValidation(config.body, b);
|
|
2876
|
-
const req = ctx.req;
|
|
2877
|
-
req._bodyValue = validBody;
|
|
2878
|
-
Object.defineProperty(req, "json", {
|
|
2879
|
-
value: async () => validBody,
|
|
2880
|
-
configurable: true
|
|
2881
|
-
});
|
|
2882
|
-
ctx.body = validBody;
|
|
2883
|
-
}
|
|
2884
|
-
if (ctx.app?.applicationConfig.hooks?.afterValidate) {
|
|
2885
|
-
const validatedData = { ...dataToValidate };
|
|
2886
|
-
if (config.params) validatedData.params = ctx.params;
|
|
2887
|
-
if (config.query) validatedData.query = validQuery;
|
|
2888
|
-
if (config.body) validatedData.body = validBody;
|
|
2889
|
-
await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
|
|
2890
|
-
}
|
|
2891
|
-
return next();
|
|
2892
|
-
};
|
|
2893
|
-
}
|
|
2894
|
-
async function runValidation(schema, data) {
|
|
2895
|
-
if (isZod(schema)) {
|
|
2896
|
-
return validateZod(schema, data);
|
|
2897
|
-
}
|
|
2898
|
-
if (isTypeBox(schema)) {
|
|
2899
|
-
return validateTypeBox(schema, data);
|
|
2900
|
-
}
|
|
2901
|
-
if (isAjv(schema)) {
|
|
2902
|
-
return validateAjv(schema, data);
|
|
2903
|
-
}
|
|
2904
|
-
if (isValibotWrapper(schema)) {
|
|
2905
|
-
return validateValibotWrapper(schema, data);
|
|
2906
|
-
}
|
|
2907
|
-
if (isClass(schema)) {
|
|
2908
|
-
return validateClassValidator(schema, data);
|
|
2909
|
-
}
|
|
2910
|
-
if (isTypeBox(schema)) {
|
|
2911
|
-
return validateTypeBox(schema, data);
|
|
2912
|
-
}
|
|
2913
|
-
if (isAjv(schema)) {
|
|
2914
|
-
return validateAjv(schema, data);
|
|
2915
|
-
}
|
|
2916
|
-
if (isValibotWrapper(schema)) {
|
|
2917
|
-
return validateValibotWrapper(schema, data);
|
|
2918
|
-
}
|
|
2919
|
-
if (typeof schema === "function") {
|
|
2920
|
-
return schema(data);
|
|
2921
|
-
}
|
|
2922
|
-
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
3796
|
+
sessionMiddleware.isBuiltin = true;
|
|
3797
|
+
sessionMiddleware.pluginName = "Session";
|
|
3798
|
+
return sessionMiddleware;
|
|
2923
3799
|
}
|
|
2924
3800
|
export {
|
|
2925
3801
|
$appRoot,
|
|
@@ -2960,6 +3836,7 @@ export {
|
|
|
2960
3836
|
Put,
|
|
2961
3837
|
Query,
|
|
2962
3838
|
RateLimit,
|
|
3839
|
+
RateLimitMiddleware,
|
|
2963
3840
|
Req,
|
|
2964
3841
|
RouteParamType,
|
|
2965
3842
|
RouterRegistry,
|
|
@@ -2975,7 +3852,11 @@ export {
|
|
|
2975
3852
|
Spec,
|
|
2976
3853
|
Use,
|
|
2977
3854
|
ValidationError,
|
|
3855
|
+
compileValidators,
|
|
2978
3856
|
compose,
|
|
3857
|
+
enableOpenApiValidation,
|
|
3858
|
+
openApiValidator,
|
|
3859
|
+
precompileValidators,
|
|
2979
3860
|
useExpress,
|
|
2980
3861
|
valibot,
|
|
2981
3862
|
validate
|