shokupan 0.2.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/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 +14 -6
- package/dist/decorators.d.ts +47 -0
- package/dist/index.cjs +895 -379
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +887 -373
- package/dist/index.js.map +1 -1
- package/dist/{openapi-analyzer-BTExMLX4.js → openapi-analyzer-BtIaHIfe.js} +11 -6
- package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
- package/dist/{openapi-analyzer-BN0wFCML.cjs → openapi-analyzer-D9YB3IkV.cjs} +11 -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 +6 -12
- 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 +28 -0
- package/dist/plugins/rate-limit.d.ts +3 -1
- package/dist/plugins/serve-static.d.ts +2 -3
- package/dist/router/trie.d.ts +14 -0
- package/dist/router.d.ts +48 -3
- package/dist/{server-adapter-CnQFr4P7.js → server-adapter-BWrEJbKL.js} +5 -5
- package/dist/server-adapter-BWrEJbKL.js.map +1 -0
- package/dist/{server-adapter-BD6oKEto.cjs → server-adapter-fVKP60e0.cjs} +5 -5
- package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
- package/dist/shokupan.d.ts +14 -3
- package/dist/types.d.ts +96 -4
- package/dist/util/cpu-monitor.d.ts +11 -0
- package/dist/util/stack.d.ts +8 -0
- package/package.json +4 -2
- package/dist/openapi-analyzer-BN0wFCML.cjs.map +0 -1
- package/dist/openapi-analyzer-BTExMLX4.js.map +0 -1
- package/dist/server-adapter-BD6oKEto.cjs.map +0 -1
- package/dist/server-adapter-CnQFr4P7.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const promises = require("node:fs/promises");
|
|
3
4
|
const eta$2 = require("eta");
|
|
4
|
-
const promises = require("fs/promises");
|
|
5
|
+
const promises$1 = require("fs/promises");
|
|
5
6
|
const path = require("path");
|
|
6
7
|
const node_async_hooks = require("node:async_hooks");
|
|
8
|
+
const node = require("@surrealdb/node");
|
|
9
|
+
const surrealdb = require("surrealdb");
|
|
7
10
|
const api = require("@opentelemetry/api");
|
|
11
|
+
const os = require("node:os");
|
|
8
12
|
const arctic = require("arctic");
|
|
9
13
|
const jose = require("jose");
|
|
14
|
+
const zlib = require("node:zlib");
|
|
10
15
|
const Ajv = require("ajv");
|
|
11
16
|
const addFormats = require("ajv-formats");
|
|
12
17
|
const classTransformer = require("class-transformer");
|
|
13
18
|
const classValidator = require("class-validator");
|
|
14
|
-
const openapiAnalyzer = require("./openapi-analyzer-
|
|
19
|
+
const openapiAnalyzer = require("./openapi-analyzer-D9YB3IkV.cjs");
|
|
15
20
|
const crypto = require("crypto");
|
|
16
21
|
const events = require("events");
|
|
17
22
|
function _interopNamespaceDefault(e) {
|
|
@@ -30,7 +35,9 @@ function _interopNamespaceDefault(e) {
|
|
|
30
35
|
n.default = e;
|
|
31
36
|
return Object.freeze(n);
|
|
32
37
|
}
|
|
38
|
+
const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
|
|
33
39
|
const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
|
|
40
|
+
const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
|
|
34
41
|
class ShokupanResponse {
|
|
35
42
|
_headers = null;
|
|
36
43
|
_status = 200;
|
|
@@ -95,10 +102,12 @@ class ShokupanResponse {
|
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
class ShokupanContext {
|
|
98
|
-
|
|
105
|
+
// Raw body for compression optimization
|
|
106
|
+
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
99
107
|
this.request = request;
|
|
100
108
|
this.server = server;
|
|
101
109
|
this.app = app;
|
|
110
|
+
this.signal = signal;
|
|
102
111
|
this.state = state || {};
|
|
103
112
|
if (enableMiddlewareTracking) {
|
|
104
113
|
const self = this;
|
|
@@ -122,7 +131,9 @@ class ShokupanContext {
|
|
|
122
131
|
state;
|
|
123
132
|
handlerStack = [];
|
|
124
133
|
response;
|
|
134
|
+
_debug;
|
|
125
135
|
_finalResponse;
|
|
136
|
+
_rawBody;
|
|
126
137
|
get url() {
|
|
127
138
|
if (!this._url) {
|
|
128
139
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -171,7 +182,17 @@ class ShokupanContext {
|
|
|
171
182
|
* Request query params
|
|
172
183
|
*/
|
|
173
184
|
get query() {
|
|
174
|
-
|
|
185
|
+
const q = {};
|
|
186
|
+
for (const [key, value] of this.url.searchParams) {
|
|
187
|
+
if (q[key] === void 0) {
|
|
188
|
+
q[key] = value;
|
|
189
|
+
} else if (Array.isArray(q[key])) {
|
|
190
|
+
q[key].push(value);
|
|
191
|
+
} else {
|
|
192
|
+
q[key] = [q[key], value];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return q;
|
|
175
196
|
}
|
|
176
197
|
/**
|
|
177
198
|
* Client IP address
|
|
@@ -246,17 +267,36 @@ class ShokupanContext {
|
|
|
246
267
|
setCookie(name, value, options = {}) {
|
|
247
268
|
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
248
269
|
if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
270
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
271
|
+
if (options.path) cookie += `; Path=${options.path || "/"}`;
|
|
249
272
|
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
250
273
|
if (options.httpOnly) cookie += `; HttpOnly`;
|
|
251
274
|
if (options.secure) cookie += `; Secure`;
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
let sameSite = options.sameSite;
|
|
276
|
+
if (sameSite === true) sameSite = "Strict";
|
|
277
|
+
if (sameSite === void 0 || sameSite === false) ;
|
|
278
|
+
else {
|
|
279
|
+
const stringSameSite = typeof sameSite === "string" ? sameSite.toLowerCase() : sameSite;
|
|
280
|
+
switch (stringSameSite) {
|
|
281
|
+
case "lax":
|
|
282
|
+
cookie += "; SameSite=Lax";
|
|
283
|
+
break;
|
|
284
|
+
case "strict":
|
|
285
|
+
cookie += "; SameSite=Strict";
|
|
286
|
+
break;
|
|
287
|
+
case "none":
|
|
288
|
+
cookie += "; SameSite=None";
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
cookie += "; SameSite=Lax";
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
257
294
|
}
|
|
258
295
|
if (options.priority) {
|
|
259
|
-
|
|
296
|
+
const p = options.priority.toLowerCase();
|
|
297
|
+
if (p === "low") cookie += "; Priority=Low";
|
|
298
|
+
else if (p === "medium") cookie += "; Priority=Medium";
|
|
299
|
+
else if (p === "high") cookie += "; Priority=High";
|
|
260
300
|
}
|
|
261
301
|
this.response.append("Set-Cookie", cookie);
|
|
262
302
|
return this;
|
|
@@ -293,6 +333,9 @@ class ShokupanContext {
|
|
|
293
333
|
send(body, options) {
|
|
294
334
|
const headers = this.mergeHeaders(options?.headers);
|
|
295
335
|
const status = options?.status ?? this.response.status;
|
|
336
|
+
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
337
|
+
this._rawBody = body;
|
|
338
|
+
}
|
|
296
339
|
this._finalResponse = new Response(body, { status, headers });
|
|
297
340
|
return this._finalResponse;
|
|
298
341
|
}
|
|
@@ -300,11 +343,11 @@ class ShokupanContext {
|
|
|
300
343
|
* Read request body
|
|
301
344
|
*/
|
|
302
345
|
async body() {
|
|
303
|
-
const contentType = this.request.headers.get("content-type");
|
|
304
|
-
if (contentType
|
|
346
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
347
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
305
348
|
return this.request.json();
|
|
306
349
|
}
|
|
307
|
-
if (contentType
|
|
350
|
+
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
308
351
|
return this.request.formData();
|
|
309
352
|
}
|
|
310
353
|
return this.request.text();
|
|
@@ -315,6 +358,7 @@ class ShokupanContext {
|
|
|
315
358
|
json(data, status, headers) {
|
|
316
359
|
const finalStatus = status ?? this.response.status;
|
|
317
360
|
const jsonString = JSON.stringify(data);
|
|
361
|
+
this._rawBody = jsonString;
|
|
318
362
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
319
363
|
this._finalResponse = new Response(jsonString, {
|
|
320
364
|
status: finalStatus,
|
|
@@ -332,15 +376,16 @@ class ShokupanContext {
|
|
|
332
376
|
*/
|
|
333
377
|
text(data, status, headers) {
|
|
334
378
|
const finalStatus = status ?? this.response.status;
|
|
379
|
+
this._rawBody = data;
|
|
335
380
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
336
381
|
this._finalResponse = new Response(data, {
|
|
337
382
|
status: finalStatus,
|
|
338
|
-
headers: { "content-type": "text/plain" }
|
|
383
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
339
384
|
});
|
|
340
385
|
return this._finalResponse;
|
|
341
386
|
}
|
|
342
387
|
const finalHeaders = this.mergeHeaders(headers);
|
|
343
|
-
finalHeaders.set("content-type", "text/plain");
|
|
388
|
+
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
344
389
|
this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
345
390
|
return this._finalResponse;
|
|
346
391
|
}
|
|
@@ -348,9 +393,10 @@ class ShokupanContext {
|
|
|
348
393
|
* Respond with HTML content
|
|
349
394
|
*/
|
|
350
395
|
html(html, status, headers) {
|
|
351
|
-
const finalHeaders = this.mergeHeaders(headers);
|
|
352
|
-
finalHeaders.set("content-type", "text/html");
|
|
353
396
|
const finalStatus = status ?? this.response.status;
|
|
397
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
398
|
+
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
399
|
+
this._rawBody = html;
|
|
354
400
|
this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
355
401
|
return this._finalResponse;
|
|
356
402
|
}
|
|
@@ -375,11 +421,20 @@ class ShokupanContext {
|
|
|
375
421
|
/**
|
|
376
422
|
* Respond with a file
|
|
377
423
|
*/
|
|
378
|
-
file(path2, fileOptions, responseOptions) {
|
|
424
|
+
async file(path2, fileOptions, responseOptions) {
|
|
379
425
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
380
426
|
const status = responseOptions?.status ?? this.response.status;
|
|
381
|
-
|
|
382
|
-
|
|
427
|
+
if (typeof Bun !== "undefined") {
|
|
428
|
+
this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
429
|
+
return this._finalResponse;
|
|
430
|
+
} else {
|
|
431
|
+
const fileBuffer = await promises.readFile(path2);
|
|
432
|
+
if (fileOptions?.type) {
|
|
433
|
+
headers.set("content-type", fileOptions.type);
|
|
434
|
+
}
|
|
435
|
+
this._finalResponse = new Response(fileBuffer, { status, headers });
|
|
436
|
+
return this._finalResponse;
|
|
437
|
+
}
|
|
383
438
|
}
|
|
384
439
|
/**
|
|
385
440
|
* JSX Rendering Function
|
|
@@ -399,6 +454,74 @@ class ShokupanContext {
|
|
|
399
454
|
return this.html(html, status, headers);
|
|
400
455
|
}
|
|
401
456
|
}
|
|
457
|
+
function RateLimitMiddleware(options = {}) {
|
|
458
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
459
|
+
const max = options.limit || options.max || 5;
|
|
460
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
461
|
+
const statusCode = options.statusCode || 429;
|
|
462
|
+
const headers = options.headers !== false;
|
|
463
|
+
const mode = options.mode || "user";
|
|
464
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
465
|
+
if (mode === "absolute") {
|
|
466
|
+
return "global";
|
|
467
|
+
}
|
|
468
|
+
return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
469
|
+
});
|
|
470
|
+
const skip = options.skip || (() => false);
|
|
471
|
+
const hits = /* @__PURE__ */ new Map();
|
|
472
|
+
const interval = setInterval(() => {
|
|
473
|
+
const now = Date.now();
|
|
474
|
+
for (const [key, record] of hits.entries()) {
|
|
475
|
+
if (record.resetTime <= now) {
|
|
476
|
+
hits.delete(key);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}, windowMs);
|
|
480
|
+
if (interval.unref) interval.unref();
|
|
481
|
+
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
482
|
+
if (skip(ctx)) return next();
|
|
483
|
+
const key = keyGenerator(ctx);
|
|
484
|
+
const now = Date.now();
|
|
485
|
+
let record = hits.get(key);
|
|
486
|
+
if (!record || record.resetTime <= now) {
|
|
487
|
+
record = {
|
|
488
|
+
hits: 0,
|
|
489
|
+
resetTime: now + windowMs
|
|
490
|
+
};
|
|
491
|
+
hits.set(key, record);
|
|
492
|
+
}
|
|
493
|
+
record.hits++;
|
|
494
|
+
const remaining = Math.max(0, max - record.hits);
|
|
495
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
496
|
+
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
497
|
+
const setHeaders = (res) => {
|
|
498
|
+
if (!headers || !res || !res.headers) return;
|
|
499
|
+
try {
|
|
500
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
501
|
+
res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
502
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
503
|
+
} catch (e) {
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
if (record.hits > max) {
|
|
507
|
+
typeof message === "object" ? JSON.stringify(message) : String(message);
|
|
508
|
+
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
509
|
+
if (headers) {
|
|
510
|
+
setHeaders(res);
|
|
511
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
512
|
+
}
|
|
513
|
+
return res;
|
|
514
|
+
}
|
|
515
|
+
const response = await next();
|
|
516
|
+
if (response instanceof Response && headers) {
|
|
517
|
+
setHeaders(response);
|
|
518
|
+
}
|
|
519
|
+
return response;
|
|
520
|
+
};
|
|
521
|
+
rateLimitMiddleware.isBuiltin = true;
|
|
522
|
+
rateLimitMiddleware.pluginName = "RateLimit";
|
|
523
|
+
return rateLimitMiddleware;
|
|
524
|
+
}
|
|
402
525
|
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
403
526
|
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
404
527
|
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
@@ -495,6 +618,9 @@ const Patch = createMethodDecorator("PATCH");
|
|
|
495
618
|
const Options = createMethodDecorator("OPTIONS");
|
|
496
619
|
const Head = createMethodDecorator("HEAD");
|
|
497
620
|
const All = createMethodDecorator("ALL");
|
|
621
|
+
function RateLimit(options) {
|
|
622
|
+
return Use(RateLimitMiddleware(options));
|
|
623
|
+
}
|
|
498
624
|
class Container {
|
|
499
625
|
static services = /* @__PURE__ */ new Map();
|
|
500
626
|
static register(target, instance) {
|
|
@@ -536,17 +662,31 @@ const compose = (middleware) => {
|
|
|
536
662
|
}
|
|
537
663
|
return function dispatch(context, next) {
|
|
538
664
|
let index = -1;
|
|
539
|
-
function runner(i) {
|
|
665
|
+
async function runner(i) {
|
|
540
666
|
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
541
667
|
index = i;
|
|
542
668
|
if (i >= middleware.length) {
|
|
543
669
|
return next ? next() : Promise.resolve();
|
|
544
670
|
}
|
|
545
671
|
const fn = middleware[i];
|
|
672
|
+
if (!context._debug) {
|
|
673
|
+
return fn(context, () => runner(i + 1));
|
|
674
|
+
}
|
|
675
|
+
const debug = context._debug;
|
|
676
|
+
const debugId = fn._debugId || fn.name || "anonymous";
|
|
677
|
+
const previousNode = debug.getCurrentNode();
|
|
678
|
+
debug.trackEdge(previousNode, debugId);
|
|
679
|
+
debug.setNode(debugId);
|
|
680
|
+
const start = performance.now();
|
|
546
681
|
try {
|
|
547
|
-
|
|
682
|
+
const res = await Promise.resolve(fn(context, () => runner(i + 1)));
|
|
683
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "success");
|
|
684
|
+
return res;
|
|
548
685
|
} catch (err) {
|
|
686
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
|
|
549
687
|
return Promise.reject(err);
|
|
688
|
+
} finally {
|
|
689
|
+
if (previousNode) debug.setNode(previousNode);
|
|
550
690
|
}
|
|
551
691
|
}
|
|
552
692
|
return runner(0);
|
|
@@ -608,6 +748,15 @@ function deepMerge(target, ...sources) {
|
|
|
608
748
|
}
|
|
609
749
|
return deepMerge(target, ...sources);
|
|
610
750
|
}
|
|
751
|
+
const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
|
|
752
|
+
const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
|
|
753
|
+
const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
|
|
754
|
+
const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
|
|
755
|
+
const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
|
|
756
|
+
const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
|
|
757
|
+
const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
|
|
758
|
+
const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
|
|
759
|
+
const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
|
|
611
760
|
function analyzeHandler(handler) {
|
|
612
761
|
const handlerSource = handler.toString();
|
|
613
762
|
const inferredSpec = {};
|
|
@@ -617,46 +766,28 @@ function analyzeHandler(handler) {
|
|
|
617
766
|
};
|
|
618
767
|
}
|
|
619
768
|
const queryParams = /* @__PURE__ */ new Map();
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
queryIntMatch.forEach((match) => {
|
|
623
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
624
|
-
if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
|
|
625
|
-
});
|
|
769
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
|
|
770
|
+
if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
|
|
626
771
|
}
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
queryFloatMatch.forEach((match) => {
|
|
630
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
631
|
-
if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
|
|
632
|
-
});
|
|
772
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
|
|
773
|
+
if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
|
|
633
774
|
}
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (paramName && !queryParams.has(paramName)) {
|
|
639
|
-
queryParams.set(paramName, { type: "number" });
|
|
640
|
-
}
|
|
641
|
-
});
|
|
775
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
|
|
776
|
+
if (match[1] && !queryParams.has(match[1])) {
|
|
777
|
+
queryParams.set(match[1], { type: "number" });
|
|
778
|
+
}
|
|
642
779
|
}
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
queryParams.set(paramName, { type: "boolean" });
|
|
649
|
-
}
|
|
650
|
-
});
|
|
780
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
|
|
781
|
+
const name = match[1] || match[2];
|
|
782
|
+
if (name && !queryParams.has(name)) {
|
|
783
|
+
queryParams.set(name, { type: "boolean" });
|
|
784
|
+
}
|
|
651
785
|
}
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
queryParams.set(paramName, { type: "string" });
|
|
658
|
-
}
|
|
659
|
-
});
|
|
786
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
|
|
787
|
+
const name = match[1];
|
|
788
|
+
if (name && !queryParams.has(name)) {
|
|
789
|
+
queryParams.set(name, { type: "string" });
|
|
790
|
+
}
|
|
660
791
|
}
|
|
661
792
|
if (queryParams.size > 0) {
|
|
662
793
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -669,19 +800,11 @@ function analyzeHandler(handler) {
|
|
|
669
800
|
});
|
|
670
801
|
}
|
|
671
802
|
const pathParams = /* @__PURE__ */ new Map();
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
paramIntMatch.forEach((match) => {
|
|
675
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
676
|
-
if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
|
|
677
|
-
});
|
|
803
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
|
|
804
|
+
if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
|
|
678
805
|
}
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
paramFloatMatch.forEach((match) => {
|
|
682
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
683
|
-
if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
|
|
684
|
-
});
|
|
806
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
|
|
807
|
+
if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
|
|
685
808
|
}
|
|
686
809
|
if (pathParams.size > 0) {
|
|
687
810
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -694,76 +817,55 @@ function analyzeHandler(handler) {
|
|
|
694
817
|
});
|
|
695
818
|
});
|
|
696
819
|
}
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
schema: { type: "string" }
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
});
|
|
820
|
+
for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
|
|
821
|
+
if (match[1]) {
|
|
822
|
+
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
823
|
+
inferredSpec.parameters.push({
|
|
824
|
+
name: match[1],
|
|
825
|
+
in: "header",
|
|
826
|
+
schema: { type: "string" }
|
|
827
|
+
});
|
|
828
|
+
}
|
|
710
829
|
}
|
|
711
830
|
const responses = {};
|
|
712
831
|
if (handlerSource.includes("ctx.json(")) {
|
|
713
832
|
responses["200"] = {
|
|
714
833
|
description: "Successful response",
|
|
715
|
-
content: {
|
|
716
|
-
"application/json": { schema: { type: "object" } }
|
|
717
|
-
}
|
|
834
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
718
835
|
};
|
|
719
836
|
}
|
|
720
837
|
if (handlerSource.includes("ctx.html(")) {
|
|
721
838
|
responses["200"] = {
|
|
722
839
|
description: "Successful response",
|
|
723
|
-
content: {
|
|
724
|
-
"text/html": { schema: { type: "string" } }
|
|
725
|
-
}
|
|
840
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
726
841
|
};
|
|
727
842
|
}
|
|
728
843
|
if (handlerSource.includes("ctx.text(")) {
|
|
729
844
|
responses["200"] = {
|
|
730
845
|
description: "Successful response",
|
|
731
|
-
content: {
|
|
732
|
-
"text/plain": { schema: { type: "string" } }
|
|
733
|
-
}
|
|
846
|
+
content: { "text/plain": { schema: { type: "string" } } }
|
|
734
847
|
};
|
|
735
848
|
}
|
|
736
849
|
if (handlerSource.includes("ctx.file(")) {
|
|
737
850
|
responses["200"] = {
|
|
738
851
|
description: "File download",
|
|
739
|
-
content: {
|
|
740
|
-
"application/octet-stream": { schema: { type: "string", format: "binary" } }
|
|
741
|
-
}
|
|
852
|
+
content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
|
|
742
853
|
};
|
|
743
854
|
}
|
|
744
855
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
745
|
-
responses["302"] = {
|
|
746
|
-
description: "Redirect"
|
|
747
|
-
};
|
|
856
|
+
responses["302"] = { description: "Redirect" };
|
|
748
857
|
}
|
|
749
858
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
750
859
|
responses["200"] = {
|
|
751
860
|
description: "Successful response",
|
|
752
|
-
content: {
|
|
753
|
-
"application/json": { schema: { type: "object" } }
|
|
754
|
-
}
|
|
861
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
755
862
|
};
|
|
756
863
|
}
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
responses[statusCode] = {
|
|
763
|
-
description: `Error response (${statusCode})`
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
});
|
|
864
|
+
for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
|
|
865
|
+
const statusCode = match[1];
|
|
866
|
+
if (statusCode && statusCode !== "200") {
|
|
867
|
+
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
868
|
+
}
|
|
767
869
|
}
|
|
768
870
|
if (Object.keys(responses).length > 0) {
|
|
769
871
|
inferredSpec.responses = responses;
|
|
@@ -777,7 +879,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
777
879
|
const defaultTagName = options.defaultTag || "Application";
|
|
778
880
|
let astRoutes = [];
|
|
779
881
|
try {
|
|
780
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-
|
|
882
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-D9YB3IkV.cjs"));
|
|
781
883
|
const analyzer = new OpenAPIAnalyzer(process.cwd());
|
|
782
884
|
const { applications } = await analyzer.analyze();
|
|
783
885
|
const appMap = /* @__PURE__ */ new Map();
|
|
@@ -1047,10 +1149,10 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1047
1149
|
};
|
|
1048
1150
|
}
|
|
1049
1151
|
const eta$1 = new eta$2.Eta();
|
|
1050
|
-
function serveStatic(
|
|
1152
|
+
function serveStatic(config, prefix) {
|
|
1051
1153
|
const rootPath = path.resolve(config.root || ".");
|
|
1052
1154
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1053
|
-
|
|
1155
|
+
const serveStaticMiddleware = async (ctx) => {
|
|
1054
1156
|
let relative = ctx.path.slice(normalizedPrefix.length);
|
|
1055
1157
|
if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
|
|
1056
1158
|
if (relative.length === 0) relative = "/";
|
|
@@ -1083,13 +1185,13 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1083
1185
|
let finalPath = requestPath;
|
|
1084
1186
|
let stats;
|
|
1085
1187
|
try {
|
|
1086
|
-
stats = await promises.stat(requestPath);
|
|
1188
|
+
stats = await promises$1.stat(requestPath);
|
|
1087
1189
|
} catch (e) {
|
|
1088
1190
|
if (config.extensions) {
|
|
1089
1191
|
for (const ext of config.extensions) {
|
|
1090
1192
|
const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
|
|
1091
1193
|
try {
|
|
1092
|
-
const s = await promises.stat(p);
|
|
1194
|
+
const s = await promises$1.stat(p);
|
|
1093
1195
|
if (s.isFile()) {
|
|
1094
1196
|
finalPath = p;
|
|
1095
1197
|
stats = s;
|
|
@@ -1118,7 +1220,7 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1118
1220
|
for (const idx of indexes) {
|
|
1119
1221
|
const idxPath = path.join(finalPath, idx);
|
|
1120
1222
|
try {
|
|
1121
|
-
const idxStats = await promises.stat(idxPath);
|
|
1223
|
+
const idxStats = await promises$1.stat(idxPath);
|
|
1122
1224
|
if (idxStats.isFile()) {
|
|
1123
1225
|
finalPath = idxPath;
|
|
1124
1226
|
foundIndex = true;
|
|
@@ -1130,7 +1232,7 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1130
1232
|
if (!foundIndex) {
|
|
1131
1233
|
if (config.listDirectory) {
|
|
1132
1234
|
try {
|
|
1133
|
-
const files = await promises.readdir(requestPath);
|
|
1235
|
+
const files = await promises$1.readdir(requestPath);
|
|
1134
1236
|
const listing = eta$1.renderString(`
|
|
1135
1237
|
<!DOCTYPE html>
|
|
1136
1238
|
<html>
|
|
@@ -1167,16 +1269,157 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1167
1269
|
}
|
|
1168
1270
|
}
|
|
1169
1271
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1272
|
+
let response;
|
|
1273
|
+
if (typeof Bun !== "undefined") {
|
|
1274
|
+
response = new Response(Bun.file(finalPath));
|
|
1275
|
+
} else {
|
|
1276
|
+
const fileBuffer = await promises$1.readFile(finalPath);
|
|
1277
|
+
response = new Response(fileBuffer);
|
|
1278
|
+
}
|
|
1172
1279
|
if (config.hooks?.onResponse) {
|
|
1173
1280
|
const hooked = await config.hooks.onResponse(ctx, response);
|
|
1174
1281
|
if (hooked) response = hooked;
|
|
1175
1282
|
}
|
|
1176
1283
|
return response;
|
|
1177
1284
|
};
|
|
1285
|
+
serveStaticMiddleware.isBuiltin = true;
|
|
1286
|
+
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1287
|
+
return serveStaticMiddleware;
|
|
1288
|
+
}
|
|
1289
|
+
class RouterTrie {
|
|
1290
|
+
root;
|
|
1291
|
+
constructor() {
|
|
1292
|
+
this.root = this.createNode();
|
|
1293
|
+
}
|
|
1294
|
+
createNode() {
|
|
1295
|
+
return {
|
|
1296
|
+
children: {}
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
insert(method, path2, handler) {
|
|
1300
|
+
let node2 = this.root;
|
|
1301
|
+
const segments = this.splitPath(path2);
|
|
1302
|
+
for (const segment of segments) {
|
|
1303
|
+
if (segment === "**") {
|
|
1304
|
+
if (!node2.recursiveChild) {
|
|
1305
|
+
node2.recursiveChild = this.createNode();
|
|
1306
|
+
}
|
|
1307
|
+
node2 = node2.recursiveChild;
|
|
1308
|
+
} else if (segment === "*") {
|
|
1309
|
+
if (!node2.wildcardChild) {
|
|
1310
|
+
node2.wildcardChild = this.createNode();
|
|
1311
|
+
}
|
|
1312
|
+
node2 = node2.wildcardChild;
|
|
1313
|
+
} else if (segment.startsWith(":")) {
|
|
1314
|
+
const paramName = segment.slice(1);
|
|
1315
|
+
if (!node2.paramChild) {
|
|
1316
|
+
node2.paramChild = this.createNode();
|
|
1317
|
+
node2.paramChild.paramName = paramName;
|
|
1318
|
+
}
|
|
1319
|
+
node2 = node2.paramChild;
|
|
1320
|
+
node2.paramName = paramName;
|
|
1321
|
+
} else {
|
|
1322
|
+
if (!node2.children[segment]) {
|
|
1323
|
+
node2.children[segment] = this.createNode();
|
|
1324
|
+
}
|
|
1325
|
+
node2 = node2.children[segment];
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (!node2.handlers) {
|
|
1329
|
+
node2.handlers = {};
|
|
1330
|
+
}
|
|
1331
|
+
node2.handlers[method] = handler;
|
|
1332
|
+
}
|
|
1333
|
+
search(method, path2) {
|
|
1334
|
+
const segments = this.splitPath(path2);
|
|
1335
|
+
const params = {};
|
|
1336
|
+
const match = this.findNode(this.root, segments, 0, params);
|
|
1337
|
+
if (match && match.handlers) {
|
|
1338
|
+
const handler = match.handlers[method] || match.handlers["ALL"];
|
|
1339
|
+
if (handler) {
|
|
1340
|
+
return { handler, params };
|
|
1341
|
+
}
|
|
1342
|
+
if (method === "HEAD" && match.handlers["GET"]) {
|
|
1343
|
+
return { handler: match.handlers["GET"], params };
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
findNode(node2, segments, index, params) {
|
|
1349
|
+
if (index === segments.length) {
|
|
1350
|
+
if (node2.handlers) return node2;
|
|
1351
|
+
if (node2.recursiveChild && node2.recursiveChild.handlers) {
|
|
1352
|
+
return node2.recursiveChild;
|
|
1353
|
+
}
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
const segment = segments[index];
|
|
1357
|
+
const child = node2.children[segment];
|
|
1358
|
+
if (child) {
|
|
1359
|
+
const result = this.findNode(child, segments, index + 1, params);
|
|
1360
|
+
if (result) return result;
|
|
1361
|
+
}
|
|
1362
|
+
if (node2.paramChild) {
|
|
1363
|
+
params[node2.paramChild.paramName] = segment;
|
|
1364
|
+
const result = this.findNode(node2.paramChild, segments, index + 1, params);
|
|
1365
|
+
if (result) return result;
|
|
1366
|
+
delete params[node2.paramChild.paramName];
|
|
1367
|
+
}
|
|
1368
|
+
if (node2.wildcardChild) {
|
|
1369
|
+
const result = this.findNode(node2.wildcardChild, segments, index + 1, params);
|
|
1370
|
+
if (result) return result;
|
|
1371
|
+
}
|
|
1372
|
+
if (node2.recursiveChild) {
|
|
1373
|
+
const remaining = segments.length - index;
|
|
1374
|
+
for (let k = 0; k <= remaining; k++) {
|
|
1375
|
+
const result = this.findNode(node2.recursiveChild, segments, index + k, params);
|
|
1376
|
+
if (result) return result;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
splitPath(path2) {
|
|
1382
|
+
if (path2 === "/" || path2 === "") return [];
|
|
1383
|
+
const s = path2.startsWith("/") ? path2.slice(1) : path2;
|
|
1384
|
+
if (s === "") return [];
|
|
1385
|
+
return s.split("/");
|
|
1386
|
+
}
|
|
1178
1387
|
}
|
|
1179
1388
|
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1389
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1390
|
+
const db = new surrealdb.Surreal({
|
|
1391
|
+
engines: node.createNodeEngines()
|
|
1392
|
+
});
|
|
1393
|
+
const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
|
|
1394
|
+
return db.query(`
|
|
1395
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1396
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1397
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1398
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1399
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1400
|
+
`);
|
|
1401
|
+
});
|
|
1402
|
+
const datastore = {
|
|
1403
|
+
get(store, key) {
|
|
1404
|
+
return db.select(new surrealdb.RecordId(store, key));
|
|
1405
|
+
},
|
|
1406
|
+
set(store, key, value) {
|
|
1407
|
+
return db.create(new surrealdb.RecordId(store, key)).content(value);
|
|
1408
|
+
},
|
|
1409
|
+
async query(query, vars) {
|
|
1410
|
+
try {
|
|
1411
|
+
const r = await db.query(query, vars).collect();
|
|
1412
|
+
return r;
|
|
1413
|
+
} catch (e) {
|
|
1414
|
+
console.error("DS ERROR:", e);
|
|
1415
|
+
throw e;
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
ready
|
|
1419
|
+
};
|
|
1420
|
+
process.on("exit", async () => {
|
|
1421
|
+
await db.close();
|
|
1422
|
+
});
|
|
1180
1423
|
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1181
1424
|
function traceHandler(fn, name) {
|
|
1182
1425
|
return async function(...args) {
|
|
@@ -1200,6 +1443,35 @@ function traceHandler(fn, name) {
|
|
|
1200
1443
|
});
|
|
1201
1444
|
};
|
|
1202
1445
|
}
|
|
1446
|
+
function getCallerInfo(skipFrames = 1) {
|
|
1447
|
+
let file = "unknown";
|
|
1448
|
+
let line = 0;
|
|
1449
|
+
try {
|
|
1450
|
+
const err = new Error();
|
|
1451
|
+
const stack = err.stack?.split("\n") || [];
|
|
1452
|
+
let found = 0;
|
|
1453
|
+
for (let i = 1; i < stack.length; i++) {
|
|
1454
|
+
const l = stack[i];
|
|
1455
|
+
if (!l.includes(":")) continue;
|
|
1456
|
+
if (l.includes("node_modules")) continue;
|
|
1457
|
+
if (l.includes("bun:main")) continue;
|
|
1458
|
+
if (l.includes("src/util/stack.ts")) continue;
|
|
1459
|
+
if (l.includes("src/router.ts")) continue;
|
|
1460
|
+
if (l.includes("src/shokupan.ts")) continue;
|
|
1461
|
+
found++;
|
|
1462
|
+
if (found >= skipFrames) {
|
|
1463
|
+
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1464
|
+
if (match) {
|
|
1465
|
+
file = match[1];
|
|
1466
|
+
line = parseInt(match[2], 10);
|
|
1467
|
+
return { file, line };
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
} catch (e) {
|
|
1472
|
+
}
|
|
1473
|
+
return { file, line };
|
|
1474
|
+
}
|
|
1203
1475
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1204
1476
|
const ShokupanApplicationTree = {};
|
|
1205
1477
|
class ShokupanRouter {
|
|
@@ -1219,6 +1491,7 @@ class ShokupanRouter {
|
|
|
1219
1491
|
[$parent] = null;
|
|
1220
1492
|
[$childRouters] = [];
|
|
1221
1493
|
[$childControllers] = [];
|
|
1494
|
+
middleware = [];
|
|
1222
1495
|
get rootConfig() {
|
|
1223
1496
|
return this[$appRoot]?.applicationConfig;
|
|
1224
1497
|
}
|
|
@@ -1227,7 +1500,54 @@ class ShokupanRouter {
|
|
|
1227
1500
|
}
|
|
1228
1501
|
[$routes] = [];
|
|
1229
1502
|
// Public via Symbol for OpenAPI generator
|
|
1503
|
+
trie = new RouterTrie();
|
|
1504
|
+
metadata;
|
|
1505
|
+
// Metadata for the router itself
|
|
1230
1506
|
currentGuards = [];
|
|
1507
|
+
// Registry Accessor
|
|
1508
|
+
getComponentRegistry() {
|
|
1509
|
+
const routes = this[$routes].map((r) => ({
|
|
1510
|
+
type: "route",
|
|
1511
|
+
path: r.path,
|
|
1512
|
+
method: r.method,
|
|
1513
|
+
metadata: r.metadata,
|
|
1514
|
+
handlerName: r.handler.name,
|
|
1515
|
+
tags: r.handlerSpec?.tags,
|
|
1516
|
+
order: r.order,
|
|
1517
|
+
_fn: r.handler
|
|
1518
|
+
// Expose handler for debugging instrumentation
|
|
1519
|
+
}));
|
|
1520
|
+
const mw = this.middleware;
|
|
1521
|
+
const middleware = mw ? mw.map((m) => ({
|
|
1522
|
+
name: m.name || "middleware",
|
|
1523
|
+
metadata: m.metadata,
|
|
1524
|
+
order: m.order,
|
|
1525
|
+
_fn: m
|
|
1526
|
+
// Expose function for debugging instrumentation
|
|
1527
|
+
})) : [];
|
|
1528
|
+
const routers = this[$childRouters].map((r) => ({
|
|
1529
|
+
type: "router",
|
|
1530
|
+
path: r[$mountPath],
|
|
1531
|
+
metadata: r.metadata,
|
|
1532
|
+
children: r.getComponentRegistry()
|
|
1533
|
+
}));
|
|
1534
|
+
const controllers = this[$childControllers].map((c) => {
|
|
1535
|
+
return {
|
|
1536
|
+
type: "controller",
|
|
1537
|
+
path: c[$mountPath] || "/",
|
|
1538
|
+
name: c.constructor.name,
|
|
1539
|
+
metadata: c.metadata
|
|
1540
|
+
// Check if we can store this
|
|
1541
|
+
};
|
|
1542
|
+
});
|
|
1543
|
+
return {
|
|
1544
|
+
metadata: this.metadata,
|
|
1545
|
+
middleware,
|
|
1546
|
+
routes,
|
|
1547
|
+
routers,
|
|
1548
|
+
controllers
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1231
1551
|
isRouterInstance(target) {
|
|
1232
1552
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1233
1553
|
}
|
|
@@ -1253,6 +1573,14 @@ class ShokupanRouter {
|
|
|
1253
1573
|
throw new Error("Router is already mounted");
|
|
1254
1574
|
}
|
|
1255
1575
|
controller[$mountPath] = prefix;
|
|
1576
|
+
if (!controller.metadata) {
|
|
1577
|
+
const info = getCallerInfo();
|
|
1578
|
+
controller.metadata = {
|
|
1579
|
+
file: info.file,
|
|
1580
|
+
line: info.line,
|
|
1581
|
+
name: "MountedRouter"
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1256
1584
|
this[$childRouters].push(controller);
|
|
1257
1585
|
controller[$parent] = this;
|
|
1258
1586
|
const setRouterContext = (router) => {
|
|
@@ -1285,6 +1613,12 @@ class ShokupanRouter {
|
|
|
1285
1613
|
}
|
|
1286
1614
|
}
|
|
1287
1615
|
instance[$mountPath] = prefix;
|
|
1616
|
+
const info = getCallerInfo();
|
|
1617
|
+
instance.metadata = {
|
|
1618
|
+
file: info.file,
|
|
1619
|
+
line: info.line,
|
|
1620
|
+
name: instance.constructor.name
|
|
1621
|
+
};
|
|
1288
1622
|
this[$childControllers].push(instance);
|
|
1289
1623
|
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1290
1624
|
const proto = Object.getPrototypeOf(instance);
|
|
@@ -1368,14 +1702,39 @@ class ShokupanRouter {
|
|
|
1368
1702
|
for (const arg of sortedArgs) {
|
|
1369
1703
|
switch (arg.type) {
|
|
1370
1704
|
case RouteParamType.BODY:
|
|
1371
|
-
|
|
1705
|
+
try {
|
|
1706
|
+
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
1707
|
+
args[arg.index] = await ctx.req.json();
|
|
1708
|
+
} else {
|
|
1709
|
+
const text = await ctx.req.text();
|
|
1710
|
+
if (!text) {
|
|
1711
|
+
args[arg.index] = {};
|
|
1712
|
+
} else {
|
|
1713
|
+
args[arg.index] = JSON.parse(text);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
} catch (e) {
|
|
1717
|
+
const err = new Error("Invalid JSON body");
|
|
1718
|
+
err.status = 400;
|
|
1719
|
+
throw err;
|
|
1720
|
+
}
|
|
1372
1721
|
break;
|
|
1373
1722
|
case RouteParamType.PARAM:
|
|
1374
1723
|
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
1375
1724
|
break;
|
|
1376
1725
|
case RouteParamType.QUERY: {
|
|
1377
1726
|
const url = new URL(ctx.req.url);
|
|
1378
|
-
|
|
1727
|
+
if (arg.name) {
|
|
1728
|
+
const vals = url.searchParams.getAll(arg.name);
|
|
1729
|
+
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1730
|
+
} else {
|
|
1731
|
+
const query = {};
|
|
1732
|
+
for (const key of url.searchParams.keys()) {
|
|
1733
|
+
const vals = url.searchParams.getAll(key);
|
|
1734
|
+
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1735
|
+
}
|
|
1736
|
+
args[arg.index] = query;
|
|
1737
|
+
}
|
|
1379
1738
|
break;
|
|
1380
1739
|
}
|
|
1381
1740
|
case RouteParamType.HEADER:
|
|
@@ -1523,30 +1882,59 @@ class ShokupanRouter {
|
|
|
1523
1882
|
data: result
|
|
1524
1883
|
};
|
|
1525
1884
|
}
|
|
1526
|
-
|
|
1885
|
+
applyRouterHooks(match) {
|
|
1527
1886
|
if (!this.config?.hooks) return match;
|
|
1528
1887
|
const hooks = this.config.hooks;
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1888
|
+
return {
|
|
1889
|
+
...match,
|
|
1890
|
+
handler: this.wrapWithHooks(match.handler, hooks)
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
wrapWithHooks(handler, hooks) {
|
|
1894
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1895
|
+
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1896
|
+
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1897
|
+
const hasError = hookList.some((h) => !!h.onError);
|
|
1898
|
+
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1899
|
+
const originalHandler = handler;
|
|
1900
|
+
const wrapped = async (ctx) => {
|
|
1901
|
+
if (hasStart) {
|
|
1902
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1903
|
+
const h = hookList[i];
|
|
1904
|
+
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
const debug = ctx._debug;
|
|
1908
|
+
let debugId;
|
|
1909
|
+
let previousNode;
|
|
1910
|
+
if (debug) {
|
|
1911
|
+
debugId = originalHandler._debugId || originalHandler.name || "handler";
|
|
1912
|
+
previousNode = debug.getCurrentNode();
|
|
1913
|
+
debug.trackEdge(previousNode, debugId);
|
|
1914
|
+
debug.setNode(debugId);
|
|
1915
|
+
}
|
|
1916
|
+
const start = performance.now();
|
|
1533
1917
|
try {
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1918
|
+
const res = await originalHandler(ctx);
|
|
1919
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1920
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1921
|
+
const h = hookList[i];
|
|
1922
|
+
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1923
|
+
}
|
|
1924
|
+
return res;
|
|
1537
1925
|
} catch (err) {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
console.error("Error in router onError hook:", e);
|
|
1543
|
-
}
|
|
1926
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1927
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1928
|
+
const h = hookList[i];
|
|
1929
|
+
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1544
1930
|
}
|
|
1545
1931
|
throw err;
|
|
1932
|
+
} finally {
|
|
1933
|
+
if (debug && previousNode) debug.setNode(previousNode);
|
|
1546
1934
|
}
|
|
1547
1935
|
};
|
|
1548
|
-
|
|
1549
|
-
return
|
|
1936
|
+
wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
|
|
1937
|
+
return wrapped;
|
|
1550
1938
|
}
|
|
1551
1939
|
/**
|
|
1552
1940
|
* Find a route matching the given method and path.
|
|
@@ -1555,24 +1943,10 @@ class ShokupanRouter {
|
|
|
1555
1943
|
* @returns Route handler and parameters if found, otherwise null
|
|
1556
1944
|
*/
|
|
1557
1945
|
find(method, path2) {
|
|
1558
|
-
|
|
1559
|
-
for (const route of routes) {
|
|
1560
|
-
if (route.method !== "ALL" && route.method !== m) continue;
|
|
1561
|
-
const match = route.regex.exec(path2);
|
|
1562
|
-
if (match) {
|
|
1563
|
-
const params = {};
|
|
1564
|
-
route.keys.forEach((key, index) => {
|
|
1565
|
-
params[key] = match[index + 1];
|
|
1566
|
-
});
|
|
1567
|
-
return this.applyHooks({ handler: route.handler, params });
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return null;
|
|
1571
|
-
};
|
|
1572
|
-
let result = findInRoutes(this[$routes], method);
|
|
1946
|
+
let result = this.trie.search(method, path2);
|
|
1573
1947
|
if (result) return result;
|
|
1574
1948
|
if (method === "HEAD") {
|
|
1575
|
-
result =
|
|
1949
|
+
result = this.trie.search("GET", path2);
|
|
1576
1950
|
if (result) return result;
|
|
1577
1951
|
}
|
|
1578
1952
|
for (const child of this[$childRouters]) {
|
|
@@ -1580,13 +1954,13 @@ class ShokupanRouter {
|
|
|
1580
1954
|
if (path2 === prefix || path2.startsWith(prefix + "/")) {
|
|
1581
1955
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1582
1956
|
const match = child.find(method, subPath);
|
|
1583
|
-
if (match) return this.
|
|
1957
|
+
if (match) return this.applyRouterHooks(match);
|
|
1584
1958
|
}
|
|
1585
1959
|
if (prefix.endsWith("/")) {
|
|
1586
1960
|
if (path2.startsWith(prefix)) {
|
|
1587
1961
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1588
1962
|
const match = child.find(method, subPath);
|
|
1589
|
-
if (match) return this.
|
|
1963
|
+
if (match) return this.applyRouterHooks(match);
|
|
1590
1964
|
}
|
|
1591
1965
|
}
|
|
1592
1966
|
}
|
|
@@ -1597,7 +1971,7 @@ class ShokupanRouter {
|
|
|
1597
1971
|
const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1598
1972
|
keys.push(key);
|
|
1599
1973
|
return "([^/]+)";
|
|
1600
|
-
}).replace(
|
|
1974
|
+
}).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
|
|
1601
1975
|
return {
|
|
1602
1976
|
regex: new RegExp(`^${pattern}$`),
|
|
1603
1977
|
keys
|
|
@@ -1666,47 +2040,84 @@ class ShokupanRouter {
|
|
|
1666
2040
|
return innerHandler(ctx);
|
|
1667
2041
|
};
|
|
1668
2042
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
try {
|
|
1672
|
-
const err = new Error();
|
|
1673
|
-
const stack = err.stack?.split("\n") || [];
|
|
1674
|
-
const callerLine = stack.find(
|
|
1675
|
-
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
1676
|
-
);
|
|
1677
|
-
if (callerLine) {
|
|
1678
|
-
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1679
|
-
if (match) {
|
|
1680
|
-
file = match[1];
|
|
1681
|
-
line = parseInt(match[2], 10);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
} catch (e) {
|
|
1685
|
-
}
|
|
1686
|
-
const trackedHandler = wrappedHandler;
|
|
2043
|
+
const { file, line } = getCallerInfo();
|
|
2044
|
+
const trackingHandler = wrappedHandler;
|
|
1687
2045
|
wrappedHandler = async (ctx) => {
|
|
1688
|
-
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1689
|
-
ctx
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2046
|
+
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2047
|
+
return trackingHandler(ctx);
|
|
2048
|
+
}
|
|
2049
|
+
const startTime = performance.now();
|
|
2050
|
+
let error = void 0;
|
|
2051
|
+
try {
|
|
2052
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2053
|
+
ctx.handlerStack.push({
|
|
2054
|
+
name: handler.name || "anonymous",
|
|
2055
|
+
file,
|
|
2056
|
+
line
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
return await trackingHandler(ctx);
|
|
2060
|
+
} catch (e) {
|
|
2061
|
+
error = e;
|
|
2062
|
+
throw e;
|
|
2063
|
+
} finally {
|
|
2064
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2065
|
+
const duration = performance.now() - startTime;
|
|
2066
|
+
const config = ctx.app.applicationConfig;
|
|
2067
|
+
try {
|
|
2068
|
+
const timestamp = Date.now();
|
|
2069
|
+
const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
|
|
2070
|
+
await datastore.set("middleware_tracking", key, {
|
|
2071
|
+
name: handler.name || "anonymous",
|
|
2072
|
+
path: ctx.path,
|
|
2073
|
+
timestamp,
|
|
2074
|
+
duration,
|
|
2075
|
+
file,
|
|
2076
|
+
line,
|
|
2077
|
+
error: error ? String(error) : void 0,
|
|
2078
|
+
metadata: {
|
|
2079
|
+
isBuiltin: handler.isBuiltin,
|
|
2080
|
+
pluginName: handler.pluginName
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2084
|
+
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2085
|
+
const cutoff = Date.now() - ttl;
|
|
2086
|
+
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2087
|
+
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2088
|
+
if (results && results[0] && results[0].count > maxCapacity) {
|
|
2089
|
+
const toDelete = results[0].count - maxCapacity;
|
|
2090
|
+
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2091
|
+
}
|
|
2092
|
+
} catch (datastoreError) {
|
|
2093
|
+
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
1694
2096
|
}
|
|
1695
|
-
return trackedHandler(ctx);
|
|
1696
2097
|
};
|
|
1697
|
-
wrappedHandler.originalHandler =
|
|
2098
|
+
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2099
|
+
let bakedHandler = wrappedHandler;
|
|
2100
|
+
if (this.config?.hooks) {
|
|
2101
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
|
|
2102
|
+
}
|
|
1698
2103
|
this[$routes].push({
|
|
1699
2104
|
method,
|
|
1700
2105
|
path: path2,
|
|
1701
|
-
regex,
|
|
1702
|
-
keys,
|
|
1703
|
-
handler
|
|
2106
|
+
regex: regex ?? new RegExp(""),
|
|
2107
|
+
keys: keys ?? [],
|
|
2108
|
+
handler,
|
|
2109
|
+
bakedHandler,
|
|
1704
2110
|
handlerSpec: spec,
|
|
1705
2111
|
group,
|
|
1706
|
-
|
|
1707
|
-
requestTimeout
|
|
1708
|
-
|
|
2112
|
+
hooks: this.config?.hooks,
|
|
2113
|
+
requestTimeout,
|
|
2114
|
+
renderer,
|
|
2115
|
+
metadata: {
|
|
2116
|
+
file,
|
|
2117
|
+
line
|
|
2118
|
+
}
|
|
1709
2119
|
});
|
|
2120
|
+
this.trie.insert(method, path2, bakedHandler);
|
|
1710
2121
|
return this;
|
|
1711
2122
|
}
|
|
1712
2123
|
get(path2, ...args) {
|
|
@@ -1780,10 +2191,10 @@ class ShokupanRouter {
|
|
|
1780
2191
|
const config = typeof options === "string" ? { root: options } : options;
|
|
1781
2192
|
const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
|
|
1782
2193
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1783
|
-
serveStatic(
|
|
2194
|
+
const handlerMiddleware = serveStatic(config, prefix);
|
|
1784
2195
|
const routeHandler = async (ctx) => {
|
|
1785
|
-
|
|
1786
|
-
|
|
2196
|
+
return handlerMiddleware(ctx, async () => {
|
|
2197
|
+
});
|
|
1787
2198
|
};
|
|
1788
2199
|
let groupName = "Static";
|
|
1789
2200
|
const segments = normalizedPrefix.split("/").filter(Boolean);
|
|
@@ -1844,6 +2255,49 @@ class ShokupanRouter {
|
|
|
1844
2255
|
return generateOpenApi(this, options);
|
|
1845
2256
|
}
|
|
1846
2257
|
}
|
|
2258
|
+
class SystemCpuMonitor {
|
|
2259
|
+
constructor(intervalMs = 1e3) {
|
|
2260
|
+
this.intervalMs = intervalMs;
|
|
2261
|
+
}
|
|
2262
|
+
interval = null;
|
|
2263
|
+
lastCpus = [];
|
|
2264
|
+
currentUsage = 0;
|
|
2265
|
+
start() {
|
|
2266
|
+
if (this.interval) return;
|
|
2267
|
+
this.lastCpus = os__namespace.cpus();
|
|
2268
|
+
this.interval = setInterval(() => this.update(), this.intervalMs);
|
|
2269
|
+
}
|
|
2270
|
+
stop() {
|
|
2271
|
+
if (this.interval) {
|
|
2272
|
+
clearInterval(this.interval);
|
|
2273
|
+
this.interval = null;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
getUsage() {
|
|
2277
|
+
return this.currentUsage;
|
|
2278
|
+
}
|
|
2279
|
+
update() {
|
|
2280
|
+
const cpus = os__namespace.cpus();
|
|
2281
|
+
let idle = 0;
|
|
2282
|
+
let total = 0;
|
|
2283
|
+
for (let i = 0; i < cpus.length; i++) {
|
|
2284
|
+
const cpu = cpus[i];
|
|
2285
|
+
const prev = this.lastCpus[i];
|
|
2286
|
+
let type;
|
|
2287
|
+
for (type in cpu.times) {
|
|
2288
|
+
const ticks = cpu.times[type];
|
|
2289
|
+
const prevTicks = prev.times[type];
|
|
2290
|
+
const diff = ticks - prevTicks;
|
|
2291
|
+
total += diff;
|
|
2292
|
+
if (type === "idle") {
|
|
2293
|
+
idle += diff;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
this.lastCpus = cpus;
|
|
2298
|
+
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
1847
2301
|
const defaults = {
|
|
1848
2302
|
port: 3e3,
|
|
1849
2303
|
hostname: "localhost",
|
|
@@ -1855,51 +2309,58 @@ api.trace.getTracer("shokupan.application");
|
|
|
1855
2309
|
class Shokupan extends ShokupanRouter {
|
|
1856
2310
|
applicationConfig = {};
|
|
1857
2311
|
openApiSpec;
|
|
1858
|
-
middleware = [];
|
|
1859
2312
|
composedMiddleware;
|
|
2313
|
+
cpuMonitor;
|
|
2314
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
2315
|
+
hooksInitialized = false;
|
|
1860
2316
|
get logger() {
|
|
1861
2317
|
return this.applicationConfig.logger;
|
|
1862
2318
|
}
|
|
1863
2319
|
constructor(applicationConfig = {}) {
|
|
1864
|
-
|
|
2320
|
+
const config = Object.assign({}, defaults, applicationConfig);
|
|
2321
|
+
const { hooks, ...routerConfig } = config;
|
|
2322
|
+
super(routerConfig);
|
|
1865
2323
|
this[$isApplication] = true;
|
|
1866
2324
|
this[$appRoot] = this;
|
|
1867
|
-
|
|
2325
|
+
this.applicationConfig = config;
|
|
2326
|
+
const { file, line } = getCallerInfo();
|
|
2327
|
+
this.metadata = {
|
|
2328
|
+
file,
|
|
2329
|
+
line,
|
|
2330
|
+
name: "ShokupanApplication"
|
|
2331
|
+
};
|
|
1868
2332
|
}
|
|
1869
2333
|
/**
|
|
1870
2334
|
* Adds middleware to the application.
|
|
1871
2335
|
*/
|
|
1872
2336
|
use(middleware) {
|
|
1873
2337
|
let trackedMiddleware = middleware;
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
if (callerLine) {
|
|
1884
|
-
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1885
|
-
if (match) {
|
|
1886
|
-
file = match[1];
|
|
1887
|
-
line = parseInt(match[2], 10);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
} catch (e) {
|
|
2338
|
+
const { file, line } = getCallerInfo();
|
|
2339
|
+
if (!middleware.metadata) {
|
|
2340
|
+
middleware.metadata = {
|
|
2341
|
+
file,
|
|
2342
|
+
line,
|
|
2343
|
+
name: middleware.name || "middleware",
|
|
2344
|
+
isBuiltin: middleware.isBuiltin,
|
|
2345
|
+
pluginName: middleware.pluginName
|
|
2346
|
+
};
|
|
1891
2347
|
}
|
|
1892
2348
|
trackedMiddleware = async (ctx, next) => {
|
|
1893
2349
|
const c = ctx;
|
|
1894
2350
|
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2351
|
+
const metadata = middleware.metadata || {};
|
|
1895
2352
|
c.handlerStack.push({
|
|
1896
|
-
name: middleware.name || "middleware",
|
|
1897
|
-
file,
|
|
1898
|
-
line
|
|
2353
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2354
|
+
file: metadata.file || file,
|
|
2355
|
+
line: metadata.line || line,
|
|
2356
|
+
isBuiltin: metadata.isBuiltin
|
|
1899
2357
|
});
|
|
1900
2358
|
}
|
|
1901
2359
|
return middleware(ctx, next);
|
|
1902
2360
|
};
|
|
2361
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2362
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2363
|
+
trackedMiddleware.order = this.middleware.length;
|
|
1903
2364
|
this.middleware.push(trackedMiddleware);
|
|
1904
2365
|
return this;
|
|
1905
2366
|
}
|
|
@@ -1911,6 +2372,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1911
2372
|
this.startupHooks.push(callback);
|
|
1912
2373
|
return this;
|
|
1913
2374
|
}
|
|
2375
|
+
specAvailableHooks = [];
|
|
2376
|
+
/**
|
|
2377
|
+
* Registers a callback to be executed when the OpenAPI spec is available.
|
|
2378
|
+
* This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
|
|
2379
|
+
*/
|
|
2380
|
+
onSpecAvailable(callback) {
|
|
2381
|
+
this.specAvailableHooks.push(callback);
|
|
2382
|
+
return this;
|
|
2383
|
+
}
|
|
1914
2384
|
/**
|
|
1915
2385
|
* Starts the application server.
|
|
1916
2386
|
*
|
|
@@ -1927,8 +2397,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1927
2397
|
}
|
|
1928
2398
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
1929
2399
|
this.openApiSpec = await generateOpenApi(this);
|
|
2400
|
+
for (const hook of this.specAvailableHooks) {
|
|
2401
|
+
await hook(this.openApiSpec);
|
|
2402
|
+
}
|
|
1930
2403
|
}
|
|
1931
2404
|
if (port === 0 && process.platform === "linux") ;
|
|
2405
|
+
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2406
|
+
this.cpuMonitor = new SystemCpuMonitor();
|
|
2407
|
+
this.cpuMonitor.start();
|
|
2408
|
+
}
|
|
1932
2409
|
const serveOptions = {
|
|
1933
2410
|
port: finalPort,
|
|
1934
2411
|
hostname: this.applicationConfig.hostname,
|
|
@@ -1953,7 +2430,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
1953
2430
|
};
|
|
1954
2431
|
let factory = this.applicationConfig.serverFactory;
|
|
1955
2432
|
if (!factory && typeof Bun === "undefined") {
|
|
1956
|
-
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-
|
|
2433
|
+
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-fVKP60e0.cjs"));
|
|
1957
2434
|
factory = createHttpServer();
|
|
1958
2435
|
}
|
|
1959
2436
|
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
@@ -2038,11 +2515,18 @@ class Shokupan extends ShokupanRouter {
|
|
|
2038
2515
|
}
|
|
2039
2516
|
async handleRequest(req, server) {
|
|
2040
2517
|
const request = req;
|
|
2041
|
-
const
|
|
2518
|
+
const controller = new AbortController();
|
|
2519
|
+
const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
|
|
2042
2520
|
const handle = async () => {
|
|
2521
|
+
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2522
|
+
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2523
|
+
const res = ctx.text(msg, 429);
|
|
2524
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2525
|
+
return res;
|
|
2526
|
+
}
|
|
2043
2527
|
try {
|
|
2044
|
-
if (this.
|
|
2045
|
-
await this.
|
|
2528
|
+
if (this.hasHook("onRequestStart")) {
|
|
2529
|
+
await this.executeHook("onRequestStart", ctx);
|
|
2046
2530
|
}
|
|
2047
2531
|
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2048
2532
|
const result = await fn(ctx, async () => {
|
|
@@ -2058,23 +2542,24 @@ class Shokupan extends ShokupanRouter {
|
|
|
2058
2542
|
response = result;
|
|
2059
2543
|
} else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
|
|
2060
2544
|
response = ctx._finalResponse;
|
|
2061
|
-
} else if ((result === null || result === void 0) && ctx.response.status === 404) {
|
|
2062
|
-
const span = asyncContext.getStore()?.get("span");
|
|
2063
|
-
if (span) span.setAttribute("http.status_code", 404);
|
|
2064
|
-
response = ctx.text("Not Found", 404);
|
|
2065
2545
|
} else if (result === null || result === void 0) {
|
|
2066
|
-
if (ctx._finalResponse
|
|
2067
|
-
|
|
2546
|
+
if (ctx._finalResponse instanceof Response) {
|
|
2547
|
+
response = ctx._finalResponse;
|
|
2548
|
+
} else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
|
|
2549
|
+
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2550
|
+
} else {
|
|
2551
|
+
response = ctx.text("Not Found", 404);
|
|
2552
|
+
}
|
|
2068
2553
|
} else if (typeof result === "object") {
|
|
2069
2554
|
response = ctx.json(result);
|
|
2070
2555
|
} else {
|
|
2071
2556
|
response = ctx.text(String(result));
|
|
2072
2557
|
}
|
|
2073
|
-
if (this.
|
|
2074
|
-
await this.
|
|
2558
|
+
if (this.hasHook("onRequestEnd")) {
|
|
2559
|
+
await this.executeHook("onRequestEnd", ctx);
|
|
2075
2560
|
}
|
|
2076
|
-
if (this.
|
|
2077
|
-
await this.
|
|
2561
|
+
if (this.hasHook("onResponseStart")) {
|
|
2562
|
+
await this.executeHook("onResponseStart", ctx, response);
|
|
2078
2563
|
}
|
|
2079
2564
|
return response;
|
|
2080
2565
|
} catch (err) {
|
|
@@ -2084,28 +2569,21 @@ class Shokupan extends ShokupanRouter {
|
|
|
2084
2569
|
const status = err.status || err.statusCode || 500;
|
|
2085
2570
|
const body = { error: err.message || "Internal Server Error" };
|
|
2086
2571
|
if (err.errors) body.errors = err.errors;
|
|
2087
|
-
if (this.
|
|
2088
|
-
|
|
2089
|
-
await this.applicationConfig.hooks.onError(err, ctx);
|
|
2090
|
-
} catch (hookErr) {
|
|
2091
|
-
console.error("Error in onError hook:", hookErr);
|
|
2092
|
-
}
|
|
2572
|
+
if (this.hasHook("onError")) {
|
|
2573
|
+
await this.executeHook("onError", err, ctx);
|
|
2093
2574
|
}
|
|
2094
2575
|
return ctx.json(body, status);
|
|
2095
2576
|
}
|
|
2096
2577
|
};
|
|
2097
2578
|
let executionPromise = handle();
|
|
2098
2579
|
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
2099
|
-
if (timeoutMs && timeoutMs > 0
|
|
2580
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2100
2581
|
let timeoutId;
|
|
2101
2582
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2102
2583
|
timeoutId = setTimeout(async () => {
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
}
|
|
2107
|
-
} catch (e) {
|
|
2108
|
-
console.error("Error in onRequestTimeout hook:", e);
|
|
2584
|
+
controller.abort();
|
|
2585
|
+
if (this.hasHook("onRequestTimeout")) {
|
|
2586
|
+
await this.executeHook("onRequestTimeout", ctx);
|
|
2109
2587
|
}
|
|
2110
2588
|
reject(new Error("Request Timeout"));
|
|
2111
2589
|
}, timeoutMs);
|
|
@@ -2119,12 +2597,56 @@ class Shokupan extends ShokupanRouter {
|
|
|
2119
2597
|
console.error("Unexpected error in request execution:", err);
|
|
2120
2598
|
return ctx.text("Internal Server Error", 500);
|
|
2121
2599
|
}).then(async (res) => {
|
|
2122
|
-
if (this.
|
|
2123
|
-
await this.
|
|
2600
|
+
if (this.hasHook("onResponseEnd")) {
|
|
2601
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2124
2602
|
}
|
|
2125
2603
|
return res;
|
|
2126
2604
|
});
|
|
2127
2605
|
}
|
|
2606
|
+
ensureHooksInitialized() {
|
|
2607
|
+
const hooks = this.applicationConfig.hooks;
|
|
2608
|
+
if (hooks) {
|
|
2609
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2610
|
+
const hookTypes = [
|
|
2611
|
+
"onRequestStart",
|
|
2612
|
+
"onRequestEnd",
|
|
2613
|
+
"onResponseStart",
|
|
2614
|
+
"onResponseEnd",
|
|
2615
|
+
"onError",
|
|
2616
|
+
"beforeValidate",
|
|
2617
|
+
"afterValidate",
|
|
2618
|
+
"onRequestTimeout",
|
|
2619
|
+
"onReadTimeout",
|
|
2620
|
+
"onWriteTimeout"
|
|
2621
|
+
];
|
|
2622
|
+
for (const type of hookTypes) {
|
|
2623
|
+
const fns = [];
|
|
2624
|
+
for (const h of hookList) {
|
|
2625
|
+
if (h[type]) fns.push(h[type]);
|
|
2626
|
+
}
|
|
2627
|
+
if (fns.length > 0) {
|
|
2628
|
+
this.hookCache.set(type, fns);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
this.hooksInitialized = true;
|
|
2633
|
+
}
|
|
2634
|
+
async executeHook(name, ...args) {
|
|
2635
|
+
if (!this.hooksInitialized) {
|
|
2636
|
+
this.ensureHooksInitialized();
|
|
2637
|
+
}
|
|
2638
|
+
const fns = this.hookCache.get(name);
|
|
2639
|
+
if (!fns) return;
|
|
2640
|
+
for (const fn of fns) {
|
|
2641
|
+
await fn(...args);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
hasHook(name) {
|
|
2645
|
+
if (!this.hooksInitialized) {
|
|
2646
|
+
this.ensureHooksInitialized();
|
|
2647
|
+
}
|
|
2648
|
+
return this.hookCache.has(name);
|
|
2649
|
+
}
|
|
2128
2650
|
}
|
|
2129
2651
|
class AuthPlugin extends ShokupanRouter {
|
|
2130
2652
|
constructor(authConfig) {
|
|
@@ -2329,7 +2851,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2329
2851
|
/**
|
|
2330
2852
|
* Middleware to verify JWT
|
|
2331
2853
|
*/
|
|
2332
|
-
|
|
2854
|
+
getMiddleware() {
|
|
2333
2855
|
return async (ctx, next) => {
|
|
2334
2856
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
2335
2857
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
@@ -2350,12 +2872,16 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2350
2872
|
}
|
|
2351
2873
|
function Compression(options = {}) {
|
|
2352
2874
|
const threshold = options.threshold ?? 512;
|
|
2353
|
-
|
|
2875
|
+
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
2354
2876
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
2355
2877
|
let method = null;
|
|
2356
2878
|
if (acceptEncoding.includes("br")) method = "br";
|
|
2357
|
-
else if (acceptEncoding.includes("zstd"))
|
|
2358
|
-
|
|
2879
|
+
else if (acceptEncoding.includes("zstd")) {
|
|
2880
|
+
if (typeof Bun === "undefined") {
|
|
2881
|
+
throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
|
|
2882
|
+
}
|
|
2883
|
+
method = "zstd";
|
|
2884
|
+
} else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
2359
2885
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
2360
2886
|
if (!method) return next();
|
|
2361
2887
|
let response = await next();
|
|
@@ -2364,8 +2890,25 @@ function Compression(options = {}) {
|
|
|
2364
2890
|
}
|
|
2365
2891
|
if (response instanceof Response) {
|
|
2366
2892
|
if (response.headers.has("Content-Encoding")) return response;
|
|
2367
|
-
|
|
2368
|
-
|
|
2893
|
+
let body;
|
|
2894
|
+
let bodySize;
|
|
2895
|
+
if (ctx._rawBody !== void 0) {
|
|
2896
|
+
if (typeof ctx._rawBody === "string") {
|
|
2897
|
+
const encoded = new TextEncoder().encode(ctx._rawBody);
|
|
2898
|
+
body = encoded.buffer;
|
|
2899
|
+
bodySize = encoded.byteLength;
|
|
2900
|
+
} else if (ctx._rawBody instanceof Uint8Array) {
|
|
2901
|
+
body = ctx._rawBody.buffer;
|
|
2902
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2903
|
+
} else {
|
|
2904
|
+
body = ctx._rawBody;
|
|
2905
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2906
|
+
}
|
|
2907
|
+
} else {
|
|
2908
|
+
body = await response.arrayBuffer();
|
|
2909
|
+
bodySize = body.byteLength;
|
|
2910
|
+
}
|
|
2911
|
+
if (bodySize < threshold) {
|
|
2369
2912
|
return new Response(body, {
|
|
2370
2913
|
status: response.status,
|
|
2371
2914
|
statusText: response.statusText,
|
|
@@ -2375,10 +2918,9 @@ function Compression(options = {}) {
|
|
|
2375
2918
|
let compressed;
|
|
2376
2919
|
switch (method) {
|
|
2377
2920
|
case "br":
|
|
2378
|
-
|
|
2379
|
-
compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
|
|
2921
|
+
compressed = await new Promise((res, rej) => zlib__namespace.brotliCompress(body, {
|
|
2380
2922
|
params: {
|
|
2381
|
-
[
|
|
2923
|
+
[zlib__namespace.constants.BROTLI_PARAM_QUALITY]: 4
|
|
2382
2924
|
}
|
|
2383
2925
|
}, (err, data) => {
|
|
2384
2926
|
if (err) return rej(err);
|
|
@@ -2386,13 +2928,19 @@ function Compression(options = {}) {
|
|
|
2386
2928
|
}));
|
|
2387
2929
|
break;
|
|
2388
2930
|
case "gzip":
|
|
2389
|
-
compressed =
|
|
2931
|
+
compressed = await new Promise((res, rej) => zlib__namespace.gzip(body, (err, data) => {
|
|
2932
|
+
if (err) return rej(err);
|
|
2933
|
+
res(data);
|
|
2934
|
+
}));
|
|
2390
2935
|
break;
|
|
2391
2936
|
case "zstd":
|
|
2392
2937
|
compressed = await Bun.zstdCompress(body);
|
|
2393
2938
|
break;
|
|
2394
2939
|
default:
|
|
2395
|
-
compressed =
|
|
2940
|
+
compressed = await new Promise((res, rej) => zlib__namespace.deflate(body, (err, data) => {
|
|
2941
|
+
if (err) return rej(err);
|
|
2942
|
+
res(data);
|
|
2943
|
+
}));
|
|
2396
2944
|
break;
|
|
2397
2945
|
}
|
|
2398
2946
|
const headers = new Headers(response.headers);
|
|
@@ -2406,6 +2954,9 @@ function Compression(options = {}) {
|
|
|
2406
2954
|
}
|
|
2407
2955
|
return response;
|
|
2408
2956
|
};
|
|
2957
|
+
compressionMiddleware.isBuiltin = true;
|
|
2958
|
+
compressionMiddleware.pluginName = "Compression";
|
|
2959
|
+
return compressionMiddleware;
|
|
2409
2960
|
}
|
|
2410
2961
|
function Cors(options = {}) {
|
|
2411
2962
|
const defaults2 = {
|
|
@@ -2415,7 +2966,7 @@ function Cors(options = {}) {
|
|
|
2415
2966
|
optionsSuccessStatus: 204
|
|
2416
2967
|
};
|
|
2417
2968
|
const opts = { ...defaults2, ...options };
|
|
2418
|
-
|
|
2969
|
+
const corsMiddleware = async function CorsMiddleware(ctx, next) {
|
|
2419
2970
|
const headers = new Headers();
|
|
2420
2971
|
const origin = ctx.headers.get("origin");
|
|
2421
2972
|
const set = (k, v) => headers.set(k, v);
|
|
@@ -2477,6 +3028,9 @@ function Cors(options = {}) {
|
|
|
2477
3028
|
}
|
|
2478
3029
|
return response;
|
|
2479
3030
|
};
|
|
3031
|
+
corsMiddleware.isBuiltin = true;
|
|
3032
|
+
corsMiddleware.pluginName = "Cors";
|
|
3033
|
+
return corsMiddleware;
|
|
2480
3034
|
}
|
|
2481
3035
|
function useExpress(expressMiddleware) {
|
|
2482
3036
|
return async (ctx, next) => {
|
|
@@ -2644,7 +3198,33 @@ const safelyGetBody = async (ctx) => {
|
|
|
2644
3198
|
return {};
|
|
2645
3199
|
}
|
|
2646
3200
|
};
|
|
3201
|
+
function getValidator(schema) {
|
|
3202
|
+
if (isZod(schema)) {
|
|
3203
|
+
return (data) => validateZod(schema, data);
|
|
3204
|
+
}
|
|
3205
|
+
if (isTypeBox(schema)) {
|
|
3206
|
+
return (data) => validateTypeBox(schema, data);
|
|
3207
|
+
}
|
|
3208
|
+
if (isAjv(schema)) {
|
|
3209
|
+
return (data) => validateAjv(schema, data);
|
|
3210
|
+
}
|
|
3211
|
+
if (isValibotWrapper(schema)) {
|
|
3212
|
+
return (data) => validateValibotWrapper(schema, data);
|
|
3213
|
+
}
|
|
3214
|
+
if (isClass(schema)) {
|
|
3215
|
+
return (data) => validateClassValidator(schema, data);
|
|
3216
|
+
}
|
|
3217
|
+
if (typeof schema === "function") {
|
|
3218
|
+
return schema;
|
|
3219
|
+
}
|
|
3220
|
+
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
3221
|
+
}
|
|
2647
3222
|
function validate(config) {
|
|
3223
|
+
const validators = {};
|
|
3224
|
+
if (config.params) validators.params = getValidator(config.params);
|
|
3225
|
+
if (config.query) validators.query = getValidator(config.query);
|
|
3226
|
+
if (config.headers) validators.headers = getValidator(config.headers);
|
|
3227
|
+
if (config.body) validators.body = getValidator(config.body);
|
|
2648
3228
|
return async (ctx, next) => {
|
|
2649
3229
|
const dataToValidate = {};
|
|
2650
3230
|
if (config.params) dataToValidate.params = ctx.params;
|
|
@@ -2663,21 +3243,21 @@ function validate(config) {
|
|
|
2663
3243
|
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
2664
3244
|
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
2665
3245
|
}
|
|
2666
|
-
if (
|
|
2667
|
-
ctx.params = await
|
|
3246
|
+
if (validators.params) {
|
|
3247
|
+
ctx.params = await validators.params(ctx.params);
|
|
2668
3248
|
}
|
|
2669
3249
|
let validQuery;
|
|
2670
|
-
if (
|
|
2671
|
-
validQuery = await
|
|
3250
|
+
if (validators.query && queryObj) {
|
|
3251
|
+
validQuery = await validators.query(queryObj);
|
|
2672
3252
|
}
|
|
2673
|
-
if (
|
|
3253
|
+
if (validators.headers) {
|
|
2674
3254
|
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2675
|
-
await
|
|
3255
|
+
await validators.headers(headersObj);
|
|
2676
3256
|
}
|
|
2677
3257
|
let validBody;
|
|
2678
|
-
if (
|
|
3258
|
+
if (validators.body) {
|
|
2679
3259
|
const b = body ?? await safelyGetBody(ctx);
|
|
2680
|
-
validBody = await
|
|
3260
|
+
validBody = await validators.body(b);
|
|
2681
3261
|
const req = ctx.req;
|
|
2682
3262
|
req._bodyValue = validBody;
|
|
2683
3263
|
Object.defineProperty(req, "json", {
|
|
@@ -2696,36 +3276,6 @@ function validate(config) {
|
|
|
2696
3276
|
return next();
|
|
2697
3277
|
};
|
|
2698
3278
|
}
|
|
2699
|
-
async function runValidation(schema, data) {
|
|
2700
|
-
if (isZod(schema)) {
|
|
2701
|
-
return validateZod(schema, data);
|
|
2702
|
-
}
|
|
2703
|
-
if (isTypeBox(schema)) {
|
|
2704
|
-
return validateTypeBox(schema, data);
|
|
2705
|
-
}
|
|
2706
|
-
if (isAjv(schema)) {
|
|
2707
|
-
return validateAjv(schema, data);
|
|
2708
|
-
}
|
|
2709
|
-
if (isValibotWrapper(schema)) {
|
|
2710
|
-
return validateValibotWrapper(schema, data);
|
|
2711
|
-
}
|
|
2712
|
-
if (isClass(schema)) {
|
|
2713
|
-
return validateClassValidator(schema, data);
|
|
2714
|
-
}
|
|
2715
|
-
if (isTypeBox(schema)) {
|
|
2716
|
-
return validateTypeBox(schema, data);
|
|
2717
|
-
}
|
|
2718
|
-
if (isAjv(schema)) {
|
|
2719
|
-
return validateAjv(schema, data);
|
|
2720
|
-
}
|
|
2721
|
-
if (isValibotWrapper(schema)) {
|
|
2722
|
-
return validateValibotWrapper(schema, data);
|
|
2723
|
-
}
|
|
2724
|
-
if (typeof schema === "function") {
|
|
2725
|
-
return schema(data);
|
|
2726
|
-
}
|
|
2727
|
-
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
2728
|
-
}
|
|
2729
3279
|
const ajv = new Ajv({ coerceTypes: true, allErrors: true });
|
|
2730
3280
|
addFormats(ajv);
|
|
2731
3281
|
const compiledValidators = /* @__PURE__ */ new WeakMap();
|
|
@@ -2740,17 +3290,18 @@ function openApiValidator() {
|
|
|
2740
3290
|
cache = compileValidators(app.openApiSpec);
|
|
2741
3291
|
compiledValidators.set(app, cache);
|
|
2742
3292
|
}
|
|
2743
|
-
const method = ctx.req.method.toLowerCase();
|
|
2744
3293
|
let matchPath;
|
|
2745
|
-
|
|
3294
|
+
let matchParams = {};
|
|
3295
|
+
if (cache.validators.has(ctx.path)) {
|
|
2746
3296
|
matchPath = ctx.path;
|
|
2747
3297
|
} else {
|
|
2748
|
-
for (const
|
|
2749
|
-
const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2750
|
-
const regex = new RegExp(regexStr);
|
|
3298
|
+
for (const [path2, { regex, paramNames }] of cache.paths) {
|
|
2751
3299
|
const match = regex.exec(ctx.path);
|
|
2752
3300
|
if (match) {
|
|
2753
|
-
matchPath =
|
|
3301
|
+
matchPath = path2;
|
|
3302
|
+
paramNames.forEach((name, i) => {
|
|
3303
|
+
matchParams[name] = match[i + 1];
|
|
3304
|
+
});
|
|
2754
3305
|
break;
|
|
2755
3306
|
}
|
|
2756
3307
|
}
|
|
@@ -2758,7 +3309,8 @@ function openApiValidator() {
|
|
|
2758
3309
|
if (!matchPath) {
|
|
2759
3310
|
return next();
|
|
2760
3311
|
}
|
|
2761
|
-
const
|
|
3312
|
+
const method = ctx.req.method.toLowerCase();
|
|
3313
|
+
const validators = cache.validators.get(matchPath)?.[method];
|
|
2762
3314
|
if (!validators) {
|
|
2763
3315
|
return next();
|
|
2764
3316
|
}
|
|
@@ -2783,21 +3335,7 @@ function openApiValidator() {
|
|
|
2783
3335
|
}
|
|
2784
3336
|
}
|
|
2785
3337
|
if (validators.params) {
|
|
2786
|
-
|
|
2787
|
-
if (Object.keys(params).length === 0 && matchPath) {
|
|
2788
|
-
const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
|
|
2789
|
-
if (paramNames.length > 0) {
|
|
2790
|
-
const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2791
|
-
const regex = new RegExp(regexStr);
|
|
2792
|
-
const match = regex.exec(ctx.path);
|
|
2793
|
-
if (match) {
|
|
2794
|
-
params = {};
|
|
2795
|
-
paramNames.forEach((name, i) => {
|
|
2796
|
-
params[name] = match[i + 1];
|
|
2797
|
-
});
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
3338
|
+
const params = { ...matchParams, ...ctx.params };
|
|
2801
3339
|
const valid = validators.params(params);
|
|
2802
3340
|
if (!valid && validators.params.errors) {
|
|
2803
3341
|
errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
|
|
@@ -2817,15 +3355,27 @@ function openApiValidator() {
|
|
|
2817
3355
|
};
|
|
2818
3356
|
}
|
|
2819
3357
|
function compileValidators(spec) {
|
|
2820
|
-
const
|
|
3358
|
+
const validators = /* @__PURE__ */ new Map();
|
|
3359
|
+
const paths = /* @__PURE__ */ new Map();
|
|
2821
3360
|
for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
|
|
3361
|
+
if (path2.includes("{")) {
|
|
3362
|
+
const paramNames = [];
|
|
3363
|
+
const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
|
|
3364
|
+
paramNames.push(name);
|
|
3365
|
+
return "([^/]+)";
|
|
3366
|
+
}) + "$";
|
|
3367
|
+
paths.set(path2, {
|
|
3368
|
+
regex: new RegExp(regexStr),
|
|
3369
|
+
paramNames
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
2822
3372
|
const pathValidators = {};
|
|
2823
3373
|
for (const [method, operation] of Object.entries(pathItem)) {
|
|
2824
3374
|
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
2825
3375
|
const oper = operation;
|
|
2826
|
-
const
|
|
3376
|
+
const opValidators = {};
|
|
2827
3377
|
if (oper.requestBody?.content?.["application/json"]?.schema) {
|
|
2828
|
-
|
|
3378
|
+
opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
|
|
2829
3379
|
}
|
|
2830
3380
|
const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
|
|
2831
3381
|
const queryProps = {};
|
|
@@ -2847,85 +3397,41 @@ function compileValidators(spec) {
|
|
|
2847
3397
|
}
|
|
2848
3398
|
}
|
|
2849
3399
|
if (Object.keys(queryProps).length > 0) {
|
|
2850
|
-
|
|
3400
|
+
opValidators.query = ajv.compile({
|
|
2851
3401
|
type: "object",
|
|
2852
3402
|
properties: queryProps,
|
|
2853
3403
|
required: queryRequired.length > 0 ? queryRequired : void 0
|
|
2854
3404
|
});
|
|
2855
3405
|
}
|
|
2856
3406
|
if (Object.keys(pathProps).length > 0) {
|
|
2857
|
-
|
|
3407
|
+
opValidators.params = ajv.compile({
|
|
2858
3408
|
type: "object",
|
|
2859
3409
|
properties: pathProps,
|
|
2860
3410
|
required: pathRequired.length > 0 ? pathRequired : void 0
|
|
2861
3411
|
});
|
|
2862
3412
|
}
|
|
2863
3413
|
if (Object.keys(headerProps).length > 0) {
|
|
2864
|
-
|
|
3414
|
+
opValidators.headers = ajv.compile({
|
|
2865
3415
|
type: "object",
|
|
2866
3416
|
properties: headerProps,
|
|
2867
3417
|
required: headerRequired.length > 0 ? headerRequired : void 0
|
|
2868
3418
|
});
|
|
2869
3419
|
}
|
|
2870
|
-
pathValidators[method] =
|
|
3420
|
+
pathValidators[method] = opValidators;
|
|
2871
3421
|
}
|
|
2872
|
-
|
|
3422
|
+
validators.set(path2, pathValidators);
|
|
2873
3423
|
}
|
|
2874
|
-
return
|
|
3424
|
+
return { paths, validators };
|
|
2875
3425
|
}
|
|
2876
|
-
function
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3426
|
+
function precompileValidators(app, spec) {
|
|
3427
|
+
const cache = compileValidators(spec);
|
|
3428
|
+
compiledValidators.set(app, cache);
|
|
3429
|
+
}
|
|
3430
|
+
function enableOpenApiValidation(app) {
|
|
3431
|
+
app.use(openApiValidator());
|
|
3432
|
+
app.onSpecAvailable((spec) => {
|
|
3433
|
+
precompileValidators(app, spec);
|
|
2884
3434
|
});
|
|
2885
|
-
const skip = options.skip || (() => false);
|
|
2886
|
-
const hits = /* @__PURE__ */ new Map();
|
|
2887
|
-
const interval = setInterval(() => {
|
|
2888
|
-
const now = Date.now();
|
|
2889
|
-
for (const [key, record] of hits.entries()) {
|
|
2890
|
-
if (record.resetTime <= now) {
|
|
2891
|
-
hits.delete(key);
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
}, windowMs);
|
|
2895
|
-
interval.unref?.();
|
|
2896
|
-
return async (ctx, next) => {
|
|
2897
|
-
if (skip(ctx)) return next();
|
|
2898
|
-
const key = keyGenerator(ctx);
|
|
2899
|
-
const now = Date.now();
|
|
2900
|
-
let record = hits.get(key);
|
|
2901
|
-
if (!record || record.resetTime <= now) {
|
|
2902
|
-
record = {
|
|
2903
|
-
hits: 0,
|
|
2904
|
-
resetTime: now + windowMs
|
|
2905
|
-
};
|
|
2906
|
-
hits.set(key, record);
|
|
2907
|
-
}
|
|
2908
|
-
record.hits++;
|
|
2909
|
-
const remaining = Math.max(0, max - record.hits);
|
|
2910
|
-
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
2911
|
-
if (record.hits > max) {
|
|
2912
|
-
if (headers) {
|
|
2913
|
-
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2914
|
-
res.headers.set("X-RateLimit-Limit", String(max));
|
|
2915
|
-
res.headers.set("X-RateLimit-Remaining", "0");
|
|
2916
|
-
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2917
|
-
return res;
|
|
2918
|
-
}
|
|
2919
|
-
return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2920
|
-
}
|
|
2921
|
-
const response = await next();
|
|
2922
|
-
if (response instanceof Response && headers) {
|
|
2923
|
-
response.headers.set("X-RateLimit-Limit", String(max));
|
|
2924
|
-
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
2925
|
-
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2926
|
-
}
|
|
2927
|
-
return response;
|
|
2928
|
-
};
|
|
2929
3435
|
}
|
|
2930
3436
|
const eta = new eta$2.Eta();
|
|
2931
3437
|
class ScalarPlugin extends ShokupanRouter {
|
|
@@ -3002,7 +3508,7 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
3002
3508
|
}
|
|
3003
3509
|
}
|
|
3004
3510
|
function SecurityHeaders(options = {}) {
|
|
3005
|
-
|
|
3511
|
+
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
3006
3512
|
const headers = {};
|
|
3007
3513
|
const set = (k, v) => headers[k] = v;
|
|
3008
3514
|
if (options.dnsPrefetchControl !== false) {
|
|
@@ -3056,6 +3562,9 @@ function SecurityHeaders(options = {}) {
|
|
|
3056
3562
|
}
|
|
3057
3563
|
return response;
|
|
3058
3564
|
};
|
|
3565
|
+
securityHeadersMiddleware.isBuiltin = true;
|
|
3566
|
+
securityHeadersMiddleware.pluginName = "SecurityHeaders";
|
|
3567
|
+
return securityHeadersMiddleware;
|
|
3059
3568
|
}
|
|
3060
3569
|
class Cookie {
|
|
3061
3570
|
maxAge;
|
|
@@ -3169,7 +3678,7 @@ function Session(options) {
|
|
|
3169
3678
|
const resave = options.resave === void 0 ? true : options.resave;
|
|
3170
3679
|
const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
|
|
3171
3680
|
const rolling = options.rolling || false;
|
|
3172
|
-
|
|
3681
|
+
const sessionMiddleware = async function SessionMiddleware(ctx, next) {
|
|
3173
3682
|
let reqSessionId = null;
|
|
3174
3683
|
const cookieHeader = ctx.req.headers.get("cookie");
|
|
3175
3684
|
const cookies = {};
|
|
@@ -3305,6 +3814,9 @@ function Session(options) {
|
|
|
3305
3814
|
}
|
|
3306
3815
|
return result;
|
|
3307
3816
|
};
|
|
3817
|
+
sessionMiddleware.isBuiltin = true;
|
|
3818
|
+
sessionMiddleware.pluginName = "Session";
|
|
3819
|
+
return sessionMiddleware;
|
|
3308
3820
|
}
|
|
3309
3821
|
exports.$appRoot = $appRoot;
|
|
3310
3822
|
exports.$childControllers = $childControllers;
|
|
@@ -3344,6 +3856,7 @@ exports.Post = Post;
|
|
|
3344
3856
|
exports.Put = Put;
|
|
3345
3857
|
exports.Query = Query;
|
|
3346
3858
|
exports.RateLimit = RateLimit;
|
|
3859
|
+
exports.RateLimitMiddleware = RateLimitMiddleware;
|
|
3347
3860
|
exports.Req = Req;
|
|
3348
3861
|
exports.RouteParamType = RouteParamType;
|
|
3349
3862
|
exports.RouterRegistry = RouterRegistry;
|
|
@@ -3359,8 +3872,11 @@ exports.ShokupanRouter = ShokupanRouter;
|
|
|
3359
3872
|
exports.Spec = Spec;
|
|
3360
3873
|
exports.Use = Use;
|
|
3361
3874
|
exports.ValidationError = ValidationError;
|
|
3875
|
+
exports.compileValidators = compileValidators;
|
|
3362
3876
|
exports.compose = compose;
|
|
3877
|
+
exports.enableOpenApiValidation = enableOpenApiValidation;
|
|
3363
3878
|
exports.openApiValidator = openApiValidator;
|
|
3879
|
+
exports.precompileValidators = precompileValidators;
|
|
3364
3880
|
exports.useExpress = useExpress;
|
|
3365
3881
|
exports.valibot = valibot;
|
|
3366
3882
|
exports.validate = validate;
|