shokupan 0.1.0 → 0.2.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/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +12 -3
- package/dist/index.cjs +819 -452
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +819 -452
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +2 -0
- package/dist/{openapi-analyzer-CFqgSLNK.cjs → openapi-analyzer-BN0wFCML.cjs} +4 -1
- package/dist/openapi-analyzer-BN0wFCML.cjs.map +1 -0
- package/dist/{openapi-analyzer-cjdGeQ5a.js → openapi-analyzer-BTExMLX4.js} +4 -1
- package/dist/openapi-analyzer-BTExMLX4.js.map +1 -0
- package/dist/plugins/debugview/plugin.d.ts +34 -0
- package/dist/plugins/openapi-validator.d.ts +2 -0
- package/dist/plugins/proxy.d.ts +9 -0
- package/dist/response.d.ts +4 -0
- package/dist/router.d.ts +2 -0
- package/dist/server-adapter-BD6oKEto.cjs +81 -0
- package/dist/server-adapter-BD6oKEto.cjs.map +1 -0
- package/dist/server-adapter-CnQFr4P7.js +64 -0
- package/dist/server-adapter-CnQFr4P7.js.map +1 -0
- package/dist/shokupan.d.ts +2 -0
- package/dist/types.d.ts +12 -0
- package/package.json +5 -2
- 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,25 @@
|
|
|
1
|
-
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
2
1
|
import { Eta } from "eta";
|
|
3
2
|
import { stat, readdir } from "fs/promises";
|
|
4
3
|
import { resolve, join, basename } from "path";
|
|
5
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
6
6
|
import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
|
|
7
7
|
import * as jose from "jose";
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import { EventEmitter } from "events";
|
|
8
|
+
import Ajv from "ajv";
|
|
9
|
+
import addFormats from "ajv-formats";
|
|
11
10
|
import { plainToInstance } from "class-transformer";
|
|
12
11
|
import { validateOrReject } from "class-validator";
|
|
12
|
+
import { OpenAPIAnalyzer } from "./openapi-analyzer-BTExMLX4.js";
|
|
13
|
+
import { randomUUID, createHmac } from "crypto";
|
|
14
|
+
import { EventEmitter } from "events";
|
|
13
15
|
class ShokupanResponse {
|
|
14
|
-
_headers =
|
|
16
|
+
_headers = null;
|
|
15
17
|
_status = 200;
|
|
16
18
|
/**
|
|
17
19
|
* Get the current headers
|
|
18
20
|
*/
|
|
19
21
|
get headers() {
|
|
22
|
+
if (!this._headers) this._headers = new Headers();
|
|
20
23
|
return this._headers;
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
@@ -37,6 +40,7 @@ class ShokupanResponse {
|
|
|
37
40
|
* @param value Header value
|
|
38
41
|
*/
|
|
39
42
|
set(key, value) {
|
|
43
|
+
if (!this._headers) this._headers = new Headers();
|
|
40
44
|
this._headers.set(key, value);
|
|
41
45
|
return this;
|
|
42
46
|
}
|
|
@@ -46,6 +50,7 @@ class ShokupanResponse {
|
|
|
46
50
|
* @param value Header value
|
|
47
51
|
*/
|
|
48
52
|
append(key, value) {
|
|
53
|
+
if (!this._headers) this._headers = new Headers();
|
|
49
54
|
this._headers.append(key, value);
|
|
50
55
|
return this;
|
|
51
56
|
}
|
|
@@ -54,29 +59,58 @@ class ShokupanResponse {
|
|
|
54
59
|
* @param key Header name
|
|
55
60
|
*/
|
|
56
61
|
get(key) {
|
|
57
|
-
return this._headers
|
|
62
|
+
return this._headers?.get(key) || null;
|
|
58
63
|
}
|
|
59
64
|
/**
|
|
60
65
|
* Check if a header exists
|
|
61
66
|
* @param key Header name
|
|
62
67
|
*/
|
|
63
68
|
has(key) {
|
|
64
|
-
return this._headers
|
|
69
|
+
return this._headers?.has(key) || false;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Internal: check if headers have been initialized/modified
|
|
73
|
+
*/
|
|
74
|
+
get hasPopulatedHeaders() {
|
|
75
|
+
return this._headers !== null;
|
|
65
76
|
}
|
|
66
77
|
}
|
|
67
78
|
class ShokupanContext {
|
|
68
|
-
constructor(request, server, state, app) {
|
|
79
|
+
constructor(request, server, state, app, enableMiddlewareTracking = false) {
|
|
69
80
|
this.request = request;
|
|
70
81
|
this.server = server;
|
|
71
82
|
this.app = app;
|
|
72
|
-
this.url = new URL(request.url);
|
|
73
83
|
this.state = state || {};
|
|
84
|
+
if (enableMiddlewareTracking) {
|
|
85
|
+
const self = this;
|
|
86
|
+
this.state = new Proxy(this.state, {
|
|
87
|
+
set(target, p, newValue, receiver) {
|
|
88
|
+
const result = Reflect.set(target, p, newValue, receiver);
|
|
89
|
+
const currentHandler = self.handlerStack[self.handlerStack.length - 1];
|
|
90
|
+
if (currentHandler) {
|
|
91
|
+
if (!currentHandler.stateChanges) currentHandler.stateChanges = {};
|
|
92
|
+
currentHandler.stateChanges[p] = newValue;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
74
98
|
this.response = new ShokupanResponse();
|
|
75
99
|
}
|
|
76
|
-
|
|
100
|
+
_url;
|
|
77
101
|
params = {};
|
|
102
|
+
// Router assigns this, but default to empty object
|
|
78
103
|
state;
|
|
104
|
+
handlerStack = [];
|
|
79
105
|
response;
|
|
106
|
+
_finalResponse;
|
|
107
|
+
get url() {
|
|
108
|
+
if (!this._url) {
|
|
109
|
+
const urlString = this.request.url || "http://localhost/";
|
|
110
|
+
this._url = new URL(urlString);
|
|
111
|
+
}
|
|
112
|
+
return this._url;
|
|
113
|
+
}
|
|
80
114
|
/**
|
|
81
115
|
* Base request
|
|
82
116
|
*/
|
|
@@ -93,7 +127,26 @@ class ShokupanContext {
|
|
|
93
127
|
* Request path
|
|
94
128
|
*/
|
|
95
129
|
get path() {
|
|
96
|
-
return this.
|
|
130
|
+
if (this._url) return this._url.pathname;
|
|
131
|
+
const url = this.request.url;
|
|
132
|
+
let queryIndex = url.indexOf("?");
|
|
133
|
+
const end = queryIndex === -1 ? url.length : queryIndex;
|
|
134
|
+
let start = 0;
|
|
135
|
+
const protocolIndex = url.indexOf("://");
|
|
136
|
+
if (protocolIndex !== -1) {
|
|
137
|
+
const hostStart = protocolIndex + 3;
|
|
138
|
+
const pathStart = url.indexOf("/", hostStart);
|
|
139
|
+
if (pathStart !== -1 && pathStart < end) {
|
|
140
|
+
start = pathStart;
|
|
141
|
+
} else {
|
|
142
|
+
return "/";
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
if (url.charCodeAt(0) === 47) {
|
|
146
|
+
start = 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return url.substring(start, end);
|
|
97
150
|
}
|
|
98
151
|
/**
|
|
99
152
|
* Request query params
|
|
@@ -190,9 +243,25 @@ class ShokupanContext {
|
|
|
190
243
|
return this;
|
|
191
244
|
}
|
|
192
245
|
mergeHeaders(headers) {
|
|
193
|
-
|
|
246
|
+
let h;
|
|
247
|
+
if (this.response.hasPopulatedHeaders) {
|
|
248
|
+
h = new Headers(this.response.headers);
|
|
249
|
+
} else {
|
|
250
|
+
h = new Headers();
|
|
251
|
+
}
|
|
194
252
|
if (headers) {
|
|
195
|
-
|
|
253
|
+
if (headers instanceof Headers) {
|
|
254
|
+
headers.forEach((v, k) => h.set(k, v));
|
|
255
|
+
} else if (Array.isArray(headers)) {
|
|
256
|
+
headers.forEach(([k, v]) => h.set(k, v));
|
|
257
|
+
} else {
|
|
258
|
+
const keys = Object.keys(headers);
|
|
259
|
+
for (let i = 0; i < keys.length; i++) {
|
|
260
|
+
const key = keys[i];
|
|
261
|
+
const val = headers[key];
|
|
262
|
+
h.set(key, val);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
196
265
|
}
|
|
197
266
|
return h;
|
|
198
267
|
}
|
|
@@ -205,7 +274,8 @@ class ShokupanContext {
|
|
|
205
274
|
send(body, options) {
|
|
206
275
|
const headers = this.mergeHeaders(options?.headers);
|
|
207
276
|
const status = options?.status ?? this.response.status;
|
|
208
|
-
|
|
277
|
+
this._finalResponse = new Response(body, { status, headers });
|
|
278
|
+
return this._finalResponse;
|
|
209
279
|
}
|
|
210
280
|
/**
|
|
211
281
|
* Read request body
|
|
@@ -224,19 +294,36 @@ class ShokupanContext {
|
|
|
224
294
|
* Respond with a JSON object
|
|
225
295
|
*/
|
|
226
296
|
json(data, status, headers) {
|
|
297
|
+
const finalStatus = status ?? this.response.status;
|
|
298
|
+
const jsonString = JSON.stringify(data);
|
|
299
|
+
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
300
|
+
this._finalResponse = new Response(jsonString, {
|
|
301
|
+
status: finalStatus,
|
|
302
|
+
headers: { "content-type": "application/json" }
|
|
303
|
+
});
|
|
304
|
+
return this._finalResponse;
|
|
305
|
+
}
|
|
227
306
|
const finalHeaders = this.mergeHeaders(headers);
|
|
228
307
|
finalHeaders.set("content-type", "application/json");
|
|
229
|
-
|
|
230
|
-
return
|
|
308
|
+
this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
|
|
309
|
+
return this._finalResponse;
|
|
231
310
|
}
|
|
232
311
|
/**
|
|
233
312
|
* Respond with a text string
|
|
234
313
|
*/
|
|
235
314
|
text(data, status, headers) {
|
|
315
|
+
const finalStatus = status ?? this.response.status;
|
|
316
|
+
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
317
|
+
this._finalResponse = new Response(data, {
|
|
318
|
+
status: finalStatus,
|
|
319
|
+
headers: { "content-type": "text/plain" }
|
|
320
|
+
});
|
|
321
|
+
return this._finalResponse;
|
|
322
|
+
}
|
|
236
323
|
const finalHeaders = this.mergeHeaders(headers);
|
|
237
324
|
finalHeaders.set("content-type", "text/plain");
|
|
238
|
-
|
|
239
|
-
return
|
|
325
|
+
this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
326
|
+
return this._finalResponse;
|
|
240
327
|
}
|
|
241
328
|
/**
|
|
242
329
|
* Respond with HTML content
|
|
@@ -245,7 +332,8 @@ class ShokupanContext {
|
|
|
245
332
|
const finalHeaders = this.mergeHeaders(headers);
|
|
246
333
|
finalHeaders.set("content-type", "text/html");
|
|
247
334
|
const finalStatus = status ?? this.response.status;
|
|
248
|
-
|
|
335
|
+
this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
336
|
+
return this._finalResponse;
|
|
249
337
|
}
|
|
250
338
|
/**
|
|
251
339
|
* Respond with a redirect
|
|
@@ -253,7 +341,8 @@ class ShokupanContext {
|
|
|
253
341
|
redirect(url, status = 302) {
|
|
254
342
|
const headers = this.mergeHeaders();
|
|
255
343
|
headers.set("Location", url);
|
|
256
|
-
|
|
344
|
+
this._finalResponse = new Response(null, { status, headers });
|
|
345
|
+
return this._finalResponse;
|
|
257
346
|
}
|
|
258
347
|
/**
|
|
259
348
|
* Respond with a status code
|
|
@@ -261,7 +350,8 @@ class ShokupanContext {
|
|
|
261
350
|
*/
|
|
262
351
|
status(status) {
|
|
263
352
|
const headers = this.mergeHeaders();
|
|
264
|
-
|
|
353
|
+
this._finalResponse = new Response(null, { status, headers });
|
|
354
|
+
return this._finalResponse;
|
|
265
355
|
}
|
|
266
356
|
/**
|
|
267
357
|
* Respond with a file
|
|
@@ -269,7 +359,8 @@ class ShokupanContext {
|
|
|
269
359
|
file(path, fileOptions, responseOptions) {
|
|
270
360
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
271
361
|
const status = responseOptions?.status ?? this.response.status;
|
|
272
|
-
|
|
362
|
+
this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
|
|
363
|
+
return this._finalResponse;
|
|
273
364
|
}
|
|
274
365
|
/**
|
|
275
366
|
* JSX Rendering Function
|
|
@@ -418,69 +509,29 @@ function Inject(token) {
|
|
|
418
509
|
});
|
|
419
510
|
};
|
|
420
511
|
}
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return result;
|
|
435
|
-
} catch (err) {
|
|
436
|
-
span.recordException(err);
|
|
437
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
438
|
-
throw err;
|
|
439
|
-
} finally {
|
|
440
|
-
span.end();
|
|
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"
|
|
512
|
+
const compose = (middleware) => {
|
|
513
|
+
if (!middleware.length) {
|
|
514
|
+
return (context2, next) => {
|
|
515
|
+
return next ? next() : Promise.resolve();
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return function dispatch(context2, next) {
|
|
519
|
+
let index = -1;
|
|
520
|
+
function runner(i) {
|
|
521
|
+
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
522
|
+
index = i;
|
|
523
|
+
if (i >= middleware.length) {
|
|
524
|
+
return next ? next() : Promise.resolve();
|
|
452
525
|
}
|
|
453
|
-
|
|
526
|
+
const fn = middleware[i];
|
|
454
527
|
try {
|
|
455
|
-
|
|
456
|
-
return result;
|
|
528
|
+
return Promise.resolve(fn(context2, () => runner(i + 1)));
|
|
457
529
|
} catch (err) {
|
|
458
|
-
|
|
459
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
460
|
-
throw err;
|
|
461
|
-
} finally {
|
|
462
|
-
span.end();
|
|
530
|
+
return Promise.reject(err);
|
|
463
531
|
}
|
|
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
532
|
}
|
|
481
|
-
return runner();
|
|
482
|
-
}
|
|
483
|
-
return fn;
|
|
533
|
+
return runner(0);
|
|
534
|
+
};
|
|
484
535
|
};
|
|
485
536
|
class ShokupanRequestBase {
|
|
486
537
|
method;
|
|
@@ -707,7 +758,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
707
758
|
const defaultTagName = options.defaultTag || "Application";
|
|
708
759
|
let astRoutes = [];
|
|
709
760
|
try {
|
|
710
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-
|
|
761
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-BTExMLX4.js");
|
|
711
762
|
const analyzer = new OpenAPIAnalyzer2(process.cwd());
|
|
712
763
|
const { applications } = await analyzer.analyze();
|
|
713
764
|
const appMap = /* @__PURE__ */ new Map();
|
|
@@ -1107,6 +1158,29 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1107
1158
|
};
|
|
1108
1159
|
}
|
|
1109
1160
|
const asyncContext = new AsyncLocalStorage();
|
|
1161
|
+
const tracer = trace.getTracer("shokupan.middleware");
|
|
1162
|
+
function traceHandler(fn, name) {
|
|
1163
|
+
return async function(...args) {
|
|
1164
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1165
|
+
kind: SpanKind.INTERNAL,
|
|
1166
|
+
attributes: {
|
|
1167
|
+
"http.route": name,
|
|
1168
|
+
"component": "shokupan.route"
|
|
1169
|
+
}
|
|
1170
|
+
}, async (span) => {
|
|
1171
|
+
try {
|
|
1172
|
+
const result = await fn.apply(this, args);
|
|
1173
|
+
return result;
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
span.recordException(err);
|
|
1176
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
1177
|
+
throw err;
|
|
1178
|
+
} finally {
|
|
1179
|
+
span.end();
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1110
1184
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1111
1185
|
const ShokupanApplicationTree = {};
|
|
1112
1186
|
class ShokupanRouter {
|
|
@@ -1297,7 +1371,7 @@ class ShokupanRouter {
|
|
|
1297
1371
|
}
|
|
1298
1372
|
}
|
|
1299
1373
|
}
|
|
1300
|
-
const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
|
|
1374
|
+
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
1301
1375
|
return tracedOriginalHandler.apply(instance, args);
|
|
1302
1376
|
};
|
|
1303
1377
|
let finalHandler = wrappedHandler;
|
|
@@ -1433,6 +1507,7 @@ class ShokupanRouter {
|
|
|
1433
1507
|
applyHooks(match) {
|
|
1434
1508
|
if (!this.config?.hooks) return match;
|
|
1435
1509
|
const hooks = this.config.hooks;
|
|
1510
|
+
if (!hooks.onRequestStart && !hooks.onRequestEnd && !hooks.onError) return match;
|
|
1436
1511
|
const originalHandler = match.handler;
|
|
1437
1512
|
match.handler = async (ctx) => {
|
|
1438
1513
|
if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
|
|
@@ -1461,16 +1536,25 @@ class ShokupanRouter {
|
|
|
1461
1536
|
* @returns Route handler and parameters if found, otherwise null
|
|
1462
1537
|
*/
|
|
1463
1538
|
find(method, path) {
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1539
|
+
const findInRoutes = (routes, m) => {
|
|
1540
|
+
for (const route of routes) {
|
|
1541
|
+
if (route.method !== "ALL" && route.method !== m) continue;
|
|
1542
|
+
const match = route.regex.exec(path);
|
|
1543
|
+
if (match) {
|
|
1544
|
+
const params = {};
|
|
1545
|
+
route.keys.forEach((key, index) => {
|
|
1546
|
+
params[key] = match[index + 1];
|
|
1547
|
+
});
|
|
1548
|
+
return this.applyHooks({ handler: route.handler, params });
|
|
1549
|
+
}
|
|
1473
1550
|
}
|
|
1551
|
+
return null;
|
|
1552
|
+
};
|
|
1553
|
+
let result = findInRoutes(this[$routes], method);
|
|
1554
|
+
if (result) return result;
|
|
1555
|
+
if (method === "HEAD") {
|
|
1556
|
+
result = findInRoutes(this[$routes], "GET");
|
|
1557
|
+
if (result) return result;
|
|
1474
1558
|
}
|
|
1475
1559
|
for (const child of this[$childRouters]) {
|
|
1476
1560
|
const prefix = child[$mountPath];
|
|
@@ -1563,6 +1647,35 @@ class ShokupanRouter {
|
|
|
1563
1647
|
return innerHandler(ctx);
|
|
1564
1648
|
};
|
|
1565
1649
|
}
|
|
1650
|
+
let file = "unknown";
|
|
1651
|
+
let line = 0;
|
|
1652
|
+
try {
|
|
1653
|
+
const err = new Error();
|
|
1654
|
+
const stack = err.stack?.split("\n") || [];
|
|
1655
|
+
const callerLine = stack.find(
|
|
1656
|
+
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
1657
|
+
);
|
|
1658
|
+
if (callerLine) {
|
|
1659
|
+
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1660
|
+
if (match) {
|
|
1661
|
+
file = match[1];
|
|
1662
|
+
line = parseInt(match[2], 10);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
} catch (e) {
|
|
1666
|
+
}
|
|
1667
|
+
const trackedHandler = wrappedHandler;
|
|
1668
|
+
wrappedHandler = async (ctx) => {
|
|
1669
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1670
|
+
ctx.handlerStack.push({
|
|
1671
|
+
name: handler.name || "anonymous",
|
|
1672
|
+
file,
|
|
1673
|
+
line
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
return trackedHandler(ctx);
|
|
1677
|
+
};
|
|
1678
|
+
wrappedHandler.originalHandler = trackedHandler.originalHandler || trackedHandler;
|
|
1566
1679
|
this[$routes].push({
|
|
1567
1680
|
method,
|
|
1568
1681
|
path,
|
|
@@ -1608,7 +1721,35 @@ class ShokupanRouter {
|
|
|
1608
1721
|
guard(specOrHandler, handler) {
|
|
1609
1722
|
const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
|
|
1610
1723
|
const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
|
|
1611
|
-
|
|
1724
|
+
let file = "unknown";
|
|
1725
|
+
let line = 0;
|
|
1726
|
+
try {
|
|
1727
|
+
const err = new Error();
|
|
1728
|
+
const stack = err.stack?.split("\n") || [];
|
|
1729
|
+
const callerLine = stack.find(
|
|
1730
|
+
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
1731
|
+
);
|
|
1732
|
+
if (callerLine) {
|
|
1733
|
+
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1734
|
+
if (match) {
|
|
1735
|
+
file = match[1];
|
|
1736
|
+
line = parseInt(match[2], 10);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
} catch (e) {
|
|
1740
|
+
}
|
|
1741
|
+
const trackedGuard = async (ctx, next) => {
|
|
1742
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1743
|
+
ctx.handlerStack.push({
|
|
1744
|
+
name: guardHandler.name || "guard",
|
|
1745
|
+
file,
|
|
1746
|
+
line
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
return guardHandler(ctx, next);
|
|
1750
|
+
};
|
|
1751
|
+
trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
|
|
1752
|
+
this.currentGuards.push({ handler: trackedGuard, spec });
|
|
1612
1753
|
return this;
|
|
1613
1754
|
}
|
|
1614
1755
|
/**
|
|
@@ -1696,6 +1837,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
1696
1837
|
applicationConfig = {};
|
|
1697
1838
|
openApiSpec;
|
|
1698
1839
|
middleware = [];
|
|
1840
|
+
composedMiddleware;
|
|
1699
1841
|
get logger() {
|
|
1700
1842
|
return this.applicationConfig.logger;
|
|
1701
1843
|
}
|
|
@@ -1709,7 +1851,37 @@ class Shokupan extends ShokupanRouter {
|
|
|
1709
1851
|
* Adds middleware to the application.
|
|
1710
1852
|
*/
|
|
1711
1853
|
use(middleware) {
|
|
1712
|
-
|
|
1854
|
+
let trackedMiddleware = middleware;
|
|
1855
|
+
let file = "unknown";
|
|
1856
|
+
let line = 0;
|
|
1857
|
+
try {
|
|
1858
|
+
const err = new Error();
|
|
1859
|
+
const stack = err.stack?.split("\n") || [];
|
|
1860
|
+
const callerLine = stack.find(
|
|
1861
|
+
(l) => l.includes(":") && !l.includes("shokupan.ts") && !l.includes("router.ts") && // In case called from router?
|
|
1862
|
+
!l.includes("node_modules") && !l.includes("bun:main")
|
|
1863
|
+
);
|
|
1864
|
+
if (callerLine) {
|
|
1865
|
+
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1866
|
+
if (match) {
|
|
1867
|
+
file = match[1];
|
|
1868
|
+
line = parseInt(match[2], 10);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
} catch (e) {
|
|
1872
|
+
}
|
|
1873
|
+
trackedMiddleware = async (ctx, next) => {
|
|
1874
|
+
const c = ctx;
|
|
1875
|
+
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1876
|
+
c.handlerStack.push({
|
|
1877
|
+
name: middleware.name || "middleware",
|
|
1878
|
+
file,
|
|
1879
|
+
line
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
return middleware(ctx, next);
|
|
1883
|
+
};
|
|
1884
|
+
this.middleware.push(trackedMiddleware);
|
|
1713
1885
|
return this;
|
|
1714
1886
|
}
|
|
1715
1887
|
startupHooks = [];
|
|
@@ -1744,9 +1916,28 @@ class Shokupan extends ShokupanRouter {
|
|
|
1744
1916
|
development: this.applicationConfig.development,
|
|
1745
1917
|
fetch: this.fetch.bind(this),
|
|
1746
1918
|
reusePort: this.applicationConfig.reusePort,
|
|
1747
|
-
idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0
|
|
1919
|
+
idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
|
|
1920
|
+
websocket: {
|
|
1921
|
+
open(ws) {
|
|
1922
|
+
ws.data?.handler?.open?.(ws);
|
|
1923
|
+
},
|
|
1924
|
+
message(ws, message) {
|
|
1925
|
+
ws.data?.handler?.message?.(ws, message);
|
|
1926
|
+
},
|
|
1927
|
+
drain(ws) {
|
|
1928
|
+
ws.data?.handler?.drain?.(ws);
|
|
1929
|
+
},
|
|
1930
|
+
close(ws, code, reason) {
|
|
1931
|
+
ws.data?.handler?.close?.(ws, code, reason);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1748
1934
|
};
|
|
1749
|
-
|
|
1935
|
+
let factory = this.applicationConfig.serverFactory;
|
|
1936
|
+
if (!factory && typeof Bun === "undefined") {
|
|
1937
|
+
const { createHttpServer } = await import("./server-adapter-CnQFr4P7.js");
|
|
1938
|
+
factory = createHttpServer();
|
|
1939
|
+
}
|
|
1940
|
+
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
1750
1941
|
console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
|
|
1751
1942
|
return server;
|
|
1752
1943
|
}
|
|
@@ -1801,108 +1992,118 @@ class Shokupan extends ShokupanRouter {
|
|
|
1801
1992
|
* @returns The response to send.
|
|
1802
1993
|
*/
|
|
1803
1994
|
async fetch(req, server) {
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1995
|
+
if (this.applicationConfig.enableTracing) {
|
|
1996
|
+
const tracer2 = trace.getTracer("shokupan.application");
|
|
1997
|
+
const store = asyncContext.getStore();
|
|
1998
|
+
const attrs = {
|
|
1999
|
+
attributes: {
|
|
2000
|
+
"http.url": req.url,
|
|
2001
|
+
"http.method": req.method
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
const parent = store?.get("span");
|
|
2005
|
+
const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
|
|
2006
|
+
return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
|
|
2007
|
+
const ctxMap = /* @__PURE__ */ new Map();
|
|
2008
|
+
ctxMap.set("span", span);
|
|
2009
|
+
ctxMap.set("request", req);
|
|
2010
|
+
return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
if (this.applicationConfig.enableAsyncLocalStorage) {
|
|
1815
2014
|
const ctxMap = /* @__PURE__ */ new Map();
|
|
1816
|
-
ctxMap.set("span", span);
|
|
1817
2015
|
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);
|
|
2016
|
+
return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
|
|
2017
|
+
}
|
|
2018
|
+
return this.handleRequest(req, server);
|
|
2019
|
+
}
|
|
2020
|
+
async handleRequest(req, server) {
|
|
2021
|
+
const request = req;
|
|
2022
|
+
const ctx = new ShokupanContext(request, server, void 0, this, this.applicationConfig.enableMiddlewareTracking);
|
|
2023
|
+
const handle = async () => {
|
|
2024
|
+
try {
|
|
2025
|
+
if (this.applicationConfig.hooks?.onRequestStart) {
|
|
2026
|
+
await this.applicationConfig.hooks.onRequestStart(ctx);
|
|
2027
|
+
}
|
|
2028
|
+
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2029
|
+
const result = await fn(ctx, async () => {
|
|
2030
|
+
const match = this.find(req.method, ctx.path);
|
|
2031
|
+
if (match) {
|
|
2032
|
+
ctx.params = match.params;
|
|
2033
|
+
return match.handler(ctx);
|
|
1868
2034
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
if (
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2035
|
+
return null;
|
|
2036
|
+
});
|
|
2037
|
+
let response;
|
|
2038
|
+
if (result instanceof Response) {
|
|
2039
|
+
response = result;
|
|
2040
|
+
} else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
|
|
2041
|
+
response = ctx._finalResponse;
|
|
2042
|
+
} else if ((result === null || result === void 0) && ctx.response.status === 404) {
|
|
2043
|
+
const span = asyncContext.getStore()?.get("span");
|
|
2044
|
+
if (span) span.setAttribute("http.status_code", 404);
|
|
2045
|
+
response = ctx.text("Not Found", 404);
|
|
2046
|
+
} else if (result === null || result === void 0) {
|
|
2047
|
+
if (ctx._finalResponse) response = ctx._finalResponse;
|
|
2048
|
+
else response = ctx.text("Not Found", 404);
|
|
2049
|
+
} else if (typeof result === "object") {
|
|
2050
|
+
response = ctx.json(result);
|
|
2051
|
+
} else {
|
|
2052
|
+
response = ctx.text(String(result));
|
|
2053
|
+
}
|
|
2054
|
+
if (this.applicationConfig.hooks?.onRequestEnd) {
|
|
2055
|
+
await this.applicationConfig.hooks.onRequestEnd(ctx);
|
|
1887
2056
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2057
|
+
if (this.applicationConfig.hooks?.onResponseStart) {
|
|
2058
|
+
await this.applicationConfig.hooks.onResponseStart(ctx, response);
|
|
2059
|
+
}
|
|
2060
|
+
return response;
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
console.error(err);
|
|
2063
|
+
const span = asyncContext.getStore()?.get("span");
|
|
2064
|
+
if (span) span.setStatus({ code: 2 });
|
|
2065
|
+
const status = err.status || err.statusCode || 500;
|
|
2066
|
+
const body = { error: err.message || "Internal Server Error" };
|
|
2067
|
+
if (err.errors) body.errors = err.errors;
|
|
2068
|
+
if (this.applicationConfig.hooks?.onError) {
|
|
2069
|
+
try {
|
|
2070
|
+
await this.applicationConfig.hooks.onError(err, ctx);
|
|
2071
|
+
} catch (hookErr) {
|
|
2072
|
+
console.error("Error in onError hook:", hookErr);
|
|
1891
2073
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2074
|
+
}
|
|
2075
|
+
return ctx.json(body, status);
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
let executionPromise = handle();
|
|
2079
|
+
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
2080
|
+
if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
|
|
2081
|
+
let timeoutId;
|
|
2082
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2083
|
+
timeoutId = setTimeout(async () => {
|
|
2084
|
+
try {
|
|
2085
|
+
if (this.applicationConfig.hooks?.onRequestTimeout) {
|
|
2086
|
+
await this.applicationConfig.hooks.onRequestTimeout(ctx);
|
|
2087
|
+
}
|
|
2088
|
+
} catch (e) {
|
|
2089
|
+
console.error("Error in onRequestTimeout hook:", e);
|
|
1897
2090
|
}
|
|
1898
|
-
|
|
1899
|
-
}
|
|
1900
|
-
};
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2091
|
+
reject(new Error("Request Timeout"));
|
|
2092
|
+
}, timeoutMs);
|
|
2093
|
+
});
|
|
2094
|
+
executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
2095
|
+
}
|
|
2096
|
+
return executionPromise.catch((err) => {
|
|
2097
|
+
if (err.message === "Request Timeout") {
|
|
2098
|
+
return ctx.text("Request Timeout", 408);
|
|
2099
|
+
}
|
|
2100
|
+
console.error("Unexpected error in request execution:", err);
|
|
2101
|
+
return ctx.text("Internal Server Error", 500);
|
|
2102
|
+
}).then(async (res) => {
|
|
2103
|
+
if (this.applicationConfig.hooks?.onResponseEnd) {
|
|
2104
|
+
await this.applicationConfig.hooks.onResponseEnd(ctx, res);
|
|
1905
2105
|
}
|
|
2106
|
+
return res;
|
|
1906
2107
|
});
|
|
1907
2108
|
}
|
|
1908
2109
|
}
|
|
@@ -2129,15 +2330,19 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2129
2330
|
}
|
|
2130
2331
|
}
|
|
2131
2332
|
function Compression(options = {}) {
|
|
2132
|
-
const threshold = options.threshold ??
|
|
2333
|
+
const threshold = options.threshold ?? 512;
|
|
2133
2334
|
return async (ctx, next) => {
|
|
2134
2335
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
2135
2336
|
let method = null;
|
|
2136
2337
|
if (acceptEncoding.includes("br")) method = "br";
|
|
2338
|
+
else if (acceptEncoding.includes("zstd")) method = "zstd";
|
|
2137
2339
|
else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
2138
2340
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
2139
2341
|
if (!method) return next();
|
|
2140
|
-
|
|
2342
|
+
let response = await next();
|
|
2343
|
+
if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
|
|
2344
|
+
response = ctx._finalResponse;
|
|
2345
|
+
}
|
|
2141
2346
|
if (response instanceof Response) {
|
|
2142
2347
|
if (response.headers.has("Content-Encoding")) return response;
|
|
2143
2348
|
const body = await response.arrayBuffer();
|
|
@@ -2149,17 +2354,31 @@ function Compression(options = {}) {
|
|
|
2149
2354
|
});
|
|
2150
2355
|
}
|
|
2151
2356
|
let compressed;
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2357
|
+
switch (method) {
|
|
2358
|
+
case "br":
|
|
2359
|
+
const zlib = require("node:zlib");
|
|
2360
|
+
compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
|
|
2361
|
+
params: {
|
|
2362
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
2363
|
+
}
|
|
2364
|
+
}, (err, data) => {
|
|
2365
|
+
if (err) return rej(err);
|
|
2366
|
+
res(data);
|
|
2367
|
+
}));
|
|
2368
|
+
break;
|
|
2369
|
+
case "gzip":
|
|
2370
|
+
compressed = Bun.gzipSync(body);
|
|
2371
|
+
break;
|
|
2372
|
+
case "zstd":
|
|
2373
|
+
compressed = await Bun.zstdCompress(body);
|
|
2374
|
+
break;
|
|
2375
|
+
default:
|
|
2376
|
+
compressed = Bun.deflateSync(body);
|
|
2377
|
+
break;
|
|
2158
2378
|
}
|
|
2159
2379
|
const headers = new Headers(response.headers);
|
|
2160
2380
|
headers.set("Content-Encoding", method);
|
|
2161
2381
|
headers.set("Content-Length", String(compressed.length));
|
|
2162
|
-
headers.delete("Content-Length");
|
|
2163
2382
|
return new Response(compressed, {
|
|
2164
2383
|
status: response.status,
|
|
2165
2384
|
statusText: response.statusText,
|
|
@@ -2300,72 +2519,407 @@ function useExpress(expressMiddleware) {
|
|
|
2300
2519
|
});
|
|
2301
2520
|
};
|
|
2302
2521
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
const statusCode = options.statusCode || 429;
|
|
2308
|
-
const headers = options.headers !== false;
|
|
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
|
-
};
|
|
2356
|
-
}
|
|
2357
|
-
const eta = new Eta();
|
|
2358
|
-
class ScalarPlugin extends ShokupanRouter {
|
|
2359
|
-
constructor(pluginOptions) {
|
|
2360
|
-
super();
|
|
2361
|
-
this.pluginOptions = pluginOptions;
|
|
2362
|
-
this.init();
|
|
2522
|
+
class ValidationError extends Error {
|
|
2523
|
+
constructor(errors) {
|
|
2524
|
+
super("Validation Error");
|
|
2525
|
+
this.errors = errors;
|
|
2363
2526
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2527
|
+
status = 400;
|
|
2528
|
+
}
|
|
2529
|
+
function isZod(schema) {
|
|
2530
|
+
return typeof schema?.safeParse === "function";
|
|
2531
|
+
}
|
|
2532
|
+
async function validateZod(schema, data) {
|
|
2533
|
+
const result = await schema.safeParseAsync(data);
|
|
2534
|
+
if (!result.success) {
|
|
2535
|
+
throw new ValidationError(result.error.errors);
|
|
2536
|
+
}
|
|
2537
|
+
return result.data;
|
|
2538
|
+
}
|
|
2539
|
+
function isTypeBox(schema) {
|
|
2540
|
+
return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
|
|
2541
|
+
}
|
|
2542
|
+
function validateTypeBox(schema, data) {
|
|
2543
|
+
if (!schema.Check(data)) {
|
|
2544
|
+
throw new ValidationError([...schema.Errors(data)]);
|
|
2545
|
+
}
|
|
2546
|
+
return data;
|
|
2547
|
+
}
|
|
2548
|
+
function isAjv(schema) {
|
|
2549
|
+
return typeof schema === "function" && "errors" in schema;
|
|
2550
|
+
}
|
|
2551
|
+
function validateAjv(schema, data) {
|
|
2552
|
+
const valid = schema(data);
|
|
2553
|
+
if (!valid) {
|
|
2554
|
+
throw new ValidationError(schema.errors);
|
|
2555
|
+
}
|
|
2556
|
+
return data;
|
|
2557
|
+
}
|
|
2558
|
+
const valibot = (schema, parser) => {
|
|
2559
|
+
return {
|
|
2560
|
+
_valibot: true,
|
|
2561
|
+
schema,
|
|
2562
|
+
parser
|
|
2563
|
+
};
|
|
2564
|
+
};
|
|
2565
|
+
function isValibotWrapper(schema) {
|
|
2566
|
+
return schema?._valibot === true;
|
|
2567
|
+
}
|
|
2568
|
+
async function validateValibotWrapper(wrapper, data) {
|
|
2569
|
+
const result = await wrapper.parser(wrapper.schema, data);
|
|
2570
|
+
if (!result.success) {
|
|
2571
|
+
throw new ValidationError(result.issues);
|
|
2572
|
+
}
|
|
2573
|
+
return result.output;
|
|
2574
|
+
}
|
|
2575
|
+
function isClass(schema) {
|
|
2576
|
+
try {
|
|
2577
|
+
if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
|
|
2578
|
+
return true;
|
|
2579
|
+
}
|
|
2580
|
+
return typeof schema === "function" && schema.prototype && schema.name;
|
|
2581
|
+
} catch {
|
|
2582
|
+
return false;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
async function validateClassValidator(schema, data) {
|
|
2586
|
+
const object = plainToInstance(schema, data);
|
|
2587
|
+
try {
|
|
2588
|
+
await validateOrReject(object);
|
|
2589
|
+
return object;
|
|
2590
|
+
} catch (errors) {
|
|
2591
|
+
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
2592
|
+
property: err.property,
|
|
2593
|
+
constraints: err.constraints,
|
|
2594
|
+
children: err.children
|
|
2595
|
+
})) : errors;
|
|
2596
|
+
throw new ValidationError(formattedErrors);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
const safelyGetBody = async (ctx) => {
|
|
2600
|
+
const req = ctx.req;
|
|
2601
|
+
if (req._bodyParsed) {
|
|
2602
|
+
return req._bodyValue;
|
|
2603
|
+
}
|
|
2604
|
+
try {
|
|
2605
|
+
let data;
|
|
2606
|
+
if (typeof req.json === "function") {
|
|
2607
|
+
data = await req.json();
|
|
2608
|
+
} else {
|
|
2609
|
+
data = req.body;
|
|
2610
|
+
if (typeof data === "string") {
|
|
2611
|
+
try {
|
|
2612
|
+
data = JSON.parse(data);
|
|
2613
|
+
} catch {
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
req._bodyParsed = true;
|
|
2618
|
+
req._bodyValue = data;
|
|
2619
|
+
Object.defineProperty(req, "json", {
|
|
2620
|
+
value: async () => req._bodyValue,
|
|
2621
|
+
configurable: true
|
|
2622
|
+
});
|
|
2623
|
+
return data;
|
|
2624
|
+
} catch (e) {
|
|
2625
|
+
return {};
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
function validate(config) {
|
|
2629
|
+
return async (ctx, next) => {
|
|
2630
|
+
const dataToValidate = {};
|
|
2631
|
+
if (config.params) dataToValidate.params = ctx.params;
|
|
2632
|
+
let queryObj;
|
|
2633
|
+
if (config.query) {
|
|
2634
|
+
const url = new URL(ctx.req.url);
|
|
2635
|
+
queryObj = Object.fromEntries(url.searchParams.entries());
|
|
2636
|
+
dataToValidate.query = queryObj;
|
|
2637
|
+
}
|
|
2638
|
+
if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
|
|
2639
|
+
let body;
|
|
2640
|
+
if (config.body) {
|
|
2641
|
+
body = await safelyGetBody(ctx);
|
|
2642
|
+
dataToValidate.body = body;
|
|
2643
|
+
}
|
|
2644
|
+
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
2645
|
+
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
2646
|
+
}
|
|
2647
|
+
if (config.params) {
|
|
2648
|
+
ctx.params = await runValidation(config.params, ctx.params);
|
|
2649
|
+
}
|
|
2650
|
+
let validQuery;
|
|
2651
|
+
if (config.query && queryObj) {
|
|
2652
|
+
validQuery = await runValidation(config.query, queryObj);
|
|
2653
|
+
}
|
|
2654
|
+
if (config.headers) {
|
|
2655
|
+
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2656
|
+
await runValidation(config.headers, headersObj);
|
|
2657
|
+
}
|
|
2658
|
+
let validBody;
|
|
2659
|
+
if (config.body) {
|
|
2660
|
+
const b = body ?? await safelyGetBody(ctx);
|
|
2661
|
+
validBody = await runValidation(config.body, b);
|
|
2662
|
+
const req = ctx.req;
|
|
2663
|
+
req._bodyValue = validBody;
|
|
2664
|
+
Object.defineProperty(req, "json", {
|
|
2665
|
+
value: async () => validBody,
|
|
2666
|
+
configurable: true
|
|
2667
|
+
});
|
|
2668
|
+
ctx.body = validBody;
|
|
2669
|
+
}
|
|
2670
|
+
if (ctx.app?.applicationConfig.hooks?.afterValidate) {
|
|
2671
|
+
const validatedData = { ...dataToValidate };
|
|
2672
|
+
if (config.params) validatedData.params = ctx.params;
|
|
2673
|
+
if (config.query) validatedData.query = validQuery;
|
|
2674
|
+
if (config.body) validatedData.body = validBody;
|
|
2675
|
+
await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
|
|
2676
|
+
}
|
|
2677
|
+
return next();
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
async function runValidation(schema, data) {
|
|
2681
|
+
if (isZod(schema)) {
|
|
2682
|
+
return validateZod(schema, data);
|
|
2683
|
+
}
|
|
2684
|
+
if (isTypeBox(schema)) {
|
|
2685
|
+
return validateTypeBox(schema, data);
|
|
2686
|
+
}
|
|
2687
|
+
if (isAjv(schema)) {
|
|
2688
|
+
return validateAjv(schema, data);
|
|
2689
|
+
}
|
|
2690
|
+
if (isValibotWrapper(schema)) {
|
|
2691
|
+
return validateValibotWrapper(schema, data);
|
|
2692
|
+
}
|
|
2693
|
+
if (isClass(schema)) {
|
|
2694
|
+
return validateClassValidator(schema, data);
|
|
2695
|
+
}
|
|
2696
|
+
if (isTypeBox(schema)) {
|
|
2697
|
+
return validateTypeBox(schema, data);
|
|
2698
|
+
}
|
|
2699
|
+
if (isAjv(schema)) {
|
|
2700
|
+
return validateAjv(schema, data);
|
|
2701
|
+
}
|
|
2702
|
+
if (isValibotWrapper(schema)) {
|
|
2703
|
+
return validateValibotWrapper(schema, data);
|
|
2704
|
+
}
|
|
2705
|
+
if (typeof schema === "function") {
|
|
2706
|
+
return schema(data);
|
|
2707
|
+
}
|
|
2708
|
+
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
2709
|
+
}
|
|
2710
|
+
const ajv = new Ajv({ coerceTypes: true, allErrors: true });
|
|
2711
|
+
addFormats(ajv);
|
|
2712
|
+
const compiledValidators = /* @__PURE__ */ new WeakMap();
|
|
2713
|
+
function openApiValidator() {
|
|
2714
|
+
return async (ctx, next) => {
|
|
2715
|
+
const app = ctx.app;
|
|
2716
|
+
if (!app || !app.openApiSpec) {
|
|
2717
|
+
return next();
|
|
2718
|
+
}
|
|
2719
|
+
let cache = compiledValidators.get(app);
|
|
2720
|
+
if (!cache) {
|
|
2721
|
+
cache = compileValidators(app.openApiSpec);
|
|
2722
|
+
compiledValidators.set(app, cache);
|
|
2723
|
+
}
|
|
2724
|
+
const method = ctx.req.method.toLowerCase();
|
|
2725
|
+
let matchPath;
|
|
2726
|
+
if (cache.has(ctx.path)) {
|
|
2727
|
+
matchPath = ctx.path;
|
|
2728
|
+
} else {
|
|
2729
|
+
for (const specPath of cache.keys()) {
|
|
2730
|
+
const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2731
|
+
const regex = new RegExp(regexStr);
|
|
2732
|
+
const match = regex.exec(ctx.path);
|
|
2733
|
+
if (match) {
|
|
2734
|
+
matchPath = specPath;
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
if (!matchPath) {
|
|
2740
|
+
return next();
|
|
2741
|
+
}
|
|
2742
|
+
const validators = cache.get(matchPath)?.[method];
|
|
2743
|
+
if (!validators) {
|
|
2744
|
+
return next();
|
|
2745
|
+
}
|
|
2746
|
+
const errors = [];
|
|
2747
|
+
if (validators.body) {
|
|
2748
|
+
let body;
|
|
2749
|
+
try {
|
|
2750
|
+
body = await ctx.req.json().catch(() => ({}));
|
|
2751
|
+
} catch {
|
|
2752
|
+
body = {};
|
|
2753
|
+
}
|
|
2754
|
+
const valid = validators.body(body);
|
|
2755
|
+
if (!valid && validators.body.errors) {
|
|
2756
|
+
errors.push(...validators.body.errors.map((e) => ({ ...e, location: "body" })));
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
if (validators.query) {
|
|
2760
|
+
const query = Object.fromEntries(new URL(ctx.req.url).searchParams.entries());
|
|
2761
|
+
const valid = validators.query(query);
|
|
2762
|
+
if (!valid && validators.query.errors) {
|
|
2763
|
+
errors.push(...validators.query.errors.map((e) => ({ ...e, location: "query" })));
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
if (validators.params) {
|
|
2767
|
+
let params = ctx.params;
|
|
2768
|
+
if (Object.keys(params).length === 0 && matchPath) {
|
|
2769
|
+
const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
|
|
2770
|
+
if (paramNames.length > 0) {
|
|
2771
|
+
const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2772
|
+
const regex = new RegExp(regexStr);
|
|
2773
|
+
const match = regex.exec(ctx.path);
|
|
2774
|
+
if (match) {
|
|
2775
|
+
params = {};
|
|
2776
|
+
paramNames.forEach((name, i) => {
|
|
2777
|
+
params[name] = match[i + 1];
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
const valid = validators.params(params);
|
|
2783
|
+
if (!valid && validators.params.errors) {
|
|
2784
|
+
errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
if (validators.headers) {
|
|
2788
|
+
const headers = Object.fromEntries(ctx.req.headers.entries());
|
|
2789
|
+
const valid = validators.headers(headers);
|
|
2790
|
+
if (!valid && validators.headers.errors) {
|
|
2791
|
+
errors.push(...validators.headers.errors.map((e) => ({ ...e, location: "header" })));
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
if (errors.length > 0) {
|
|
2795
|
+
throw new ValidationError(errors);
|
|
2796
|
+
}
|
|
2797
|
+
return next();
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
function compileValidators(spec) {
|
|
2801
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2802
|
+
for (const [path, pathItem] of Object.entries(spec.paths || {})) {
|
|
2803
|
+
const pathValidators = {};
|
|
2804
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
2805
|
+
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
2806
|
+
const oper = operation;
|
|
2807
|
+
const validators = {};
|
|
2808
|
+
if (oper.requestBody?.content?.["application/json"]?.schema) {
|
|
2809
|
+
validators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
|
|
2810
|
+
}
|
|
2811
|
+
const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
|
|
2812
|
+
const queryProps = {};
|
|
2813
|
+
const pathProps = {};
|
|
2814
|
+
const headerProps = {};
|
|
2815
|
+
const queryRequired = [];
|
|
2816
|
+
const pathRequired = [];
|
|
2817
|
+
const headerRequired = [];
|
|
2818
|
+
for (const param of parameters) {
|
|
2819
|
+
if (param.in === "query") {
|
|
2820
|
+
queryProps[param.name] = param.schema || {};
|
|
2821
|
+
if (param.required) queryRequired.push(param.name);
|
|
2822
|
+
} else if (param.in === "path") {
|
|
2823
|
+
pathProps[param.name] = param.schema || {};
|
|
2824
|
+
pathRequired.push(param.name);
|
|
2825
|
+
} else if (param.in === "header") {
|
|
2826
|
+
headerProps[param.name] = param.schema || {};
|
|
2827
|
+
if (param.required) headerRequired.push(param.name);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
if (Object.keys(queryProps).length > 0) {
|
|
2831
|
+
validators.query = ajv.compile({
|
|
2832
|
+
type: "object",
|
|
2833
|
+
properties: queryProps,
|
|
2834
|
+
required: queryRequired.length > 0 ? queryRequired : void 0
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
if (Object.keys(pathProps).length > 0) {
|
|
2838
|
+
validators.params = ajv.compile({
|
|
2839
|
+
type: "object",
|
|
2840
|
+
properties: pathProps,
|
|
2841
|
+
required: pathRequired.length > 0 ? pathRequired : void 0
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
if (Object.keys(headerProps).length > 0) {
|
|
2845
|
+
validators.headers = ajv.compile({
|
|
2846
|
+
type: "object",
|
|
2847
|
+
properties: headerProps,
|
|
2848
|
+
required: headerRequired.length > 0 ? headerRequired : void 0
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
pathValidators[method] = validators;
|
|
2852
|
+
}
|
|
2853
|
+
cache.set(path, pathValidators);
|
|
2854
|
+
}
|
|
2855
|
+
return cache;
|
|
2856
|
+
}
|
|
2857
|
+
function RateLimit(options = {}) {
|
|
2858
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
2859
|
+
const max = options.max || 5;
|
|
2860
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
2861
|
+
const statusCode = options.statusCode || 429;
|
|
2862
|
+
const headers = options.headers !== false;
|
|
2863
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
2864
|
+
return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
|
|
2865
|
+
});
|
|
2866
|
+
const skip = options.skip || (() => false);
|
|
2867
|
+
const hits = /* @__PURE__ */ new Map();
|
|
2868
|
+
const interval = setInterval(() => {
|
|
2869
|
+
const now = Date.now();
|
|
2870
|
+
for (const [key, record] of hits.entries()) {
|
|
2871
|
+
if (record.resetTime <= now) {
|
|
2872
|
+
hits.delete(key);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}, windowMs);
|
|
2876
|
+
interval.unref?.();
|
|
2877
|
+
return async (ctx, next) => {
|
|
2878
|
+
if (skip(ctx)) return next();
|
|
2879
|
+
const key = keyGenerator(ctx);
|
|
2880
|
+
const now = Date.now();
|
|
2881
|
+
let record = hits.get(key);
|
|
2882
|
+
if (!record || record.resetTime <= now) {
|
|
2883
|
+
record = {
|
|
2884
|
+
hits: 0,
|
|
2885
|
+
resetTime: now + windowMs
|
|
2886
|
+
};
|
|
2887
|
+
hits.set(key, record);
|
|
2888
|
+
}
|
|
2889
|
+
record.hits++;
|
|
2890
|
+
const remaining = Math.max(0, max - record.hits);
|
|
2891
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
2892
|
+
if (record.hits > max) {
|
|
2893
|
+
if (headers) {
|
|
2894
|
+
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2895
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
2896
|
+
res.headers.set("X-RateLimit-Remaining", "0");
|
|
2897
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2898
|
+
return res;
|
|
2899
|
+
}
|
|
2900
|
+
return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2901
|
+
}
|
|
2902
|
+
const response = await next();
|
|
2903
|
+
if (response instanceof Response && headers) {
|
|
2904
|
+
response.headers.set("X-RateLimit-Limit", String(max));
|
|
2905
|
+
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
2906
|
+
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2907
|
+
}
|
|
2908
|
+
return response;
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
const eta = new Eta();
|
|
2912
|
+
class ScalarPlugin extends ShokupanRouter {
|
|
2913
|
+
constructor(pluginOptions) {
|
|
2914
|
+
super();
|
|
2915
|
+
this.pluginOptions = pluginOptions;
|
|
2916
|
+
this.init();
|
|
2917
|
+
}
|
|
2918
|
+
init() {
|
|
2919
|
+
this.get("/", (ctx) => {
|
|
2920
|
+
let path = ctx.url.toString();
|
|
2921
|
+
if (!path.endsWith("/")) path += "/";
|
|
2922
|
+
return ctx.html(eta.renderString(`<!doctype html>
|
|
2369
2923
|
<html>
|
|
2370
2924
|
<head>
|
|
2371
2925
|
<title>API Reference</title>
|
|
@@ -2733,194 +3287,6 @@ function Session(options) {
|
|
|
2733
3287
|
return result;
|
|
2734
3288
|
};
|
|
2735
3289
|
}
|
|
2736
|
-
class ValidationError extends Error {
|
|
2737
|
-
constructor(errors) {
|
|
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.");
|
|
2923
|
-
}
|
|
2924
3290
|
export {
|
|
2925
3291
|
$appRoot,
|
|
2926
3292
|
$childControllers,
|
|
@@ -2976,6 +3342,7 @@ export {
|
|
|
2976
3342
|
Use,
|
|
2977
3343
|
ValidationError,
|
|
2978
3344
|
compose,
|
|
3345
|
+
openApiValidator,
|
|
2979
3346
|
useExpress,
|
|
2980
3347
|
valibot,
|
|
2981
3348
|
validate
|