shokupan 0.11.0 → 0.12.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 +46 -1815
- package/dist/{analyzer-CnKnQ5KV.js → analyzer-BkNQHWj4.js} +2 -2
- package/dist/{analyzer-CnKnQ5KV.js.map → analyzer-BkNQHWj4.js.map} +1 -1
- package/dist/{analyzer-BAhvpNY_.cjs → analyzer-DM-OlRq8.cjs} +2 -2
- package/dist/{analyzer-BAhvpNY_.cjs.map → analyzer-DM-OlRq8.cjs.map} +1 -1
- package/dist/{analyzer.impl-CfpMu4-g.cjs → analyzer.impl-CVJ8zfGQ.cjs} +11 -3
- package/dist/analyzer.impl-CVJ8zfGQ.cjs.map +1 -0
- package/dist/{analyzer.impl-DCiqlXI5.js → analyzer.impl-CsA1bS_s.js} +11 -3
- package/dist/analyzer.impl-CsA1bS_s.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +40 -8
- package/dist/index.cjs +1011 -300
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1011 -300
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/static/theme.css +4 -0
- package/dist/plugins/application/auth.d.ts +5 -0
- package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +12 -0
- package/dist/plugins/application/dashboard/plugin.d.ts +9 -0
- package/dist/plugins/application/dashboard/static/requests.js +537 -251
- package/dist/plugins/application/dashboard/static/tabulator.css +23 -3
- package/dist/plugins/application/dashboard/static/theme.css +4 -0
- package/dist/plugins/application/mcp-server/plugin.d.ts +39 -0
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +4 -0
- package/dist/plugins/middleware/compression.d.ts +12 -2
- package/dist/plugins/middleware/rate-limit.d.ts +5 -0
- package/dist/router.d.ts +6 -5
- package/dist/server.d.ts +22 -0
- package/dist/shokupan.d.ts +24 -1
- package/dist/util/adapter/bun.d.ts +8 -0
- package/dist/util/adapter/index.d.ts +4 -0
- package/dist/util/adapter/interface.d.ts +12 -0
- package/dist/util/adapter/node.d.ts +8 -0
- package/dist/util/adapter/wintercg.d.ts +5 -0
- package/dist/util/body-parser.d.ts +30 -0
- package/dist/util/decorators.d.ts +20 -3
- package/dist/util/di.d.ts +3 -8
- package/dist/util/metadata.d.ts +18 -0
- package/dist/util/request.d.ts +1 -0
- package/dist/util/symbol.d.ts +1 -0
- package/dist/util/types.d.ts +132 -3
- package/package.json +3 -1
- package/dist/analyzer.impl-CfpMu4-g.cjs.map +0 -1
- package/dist/analyzer.impl-DCiqlXI5.js.map +0 -1
- package/dist/plugins/application/dashboard/static/failures.js +0 -85
- package/dist/plugins/application/http-server.d.ts +0 -13
- package/dist/util/adapter/adapters.d.ts +0 -19
package/dist/index.js
CHANGED
|
@@ -21,12 +21,121 @@ import os__default from "node:os";
|
|
|
21
21
|
import { createRequire } from "node:module";
|
|
22
22
|
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
23
23
|
import { readFileSync } from "node:fs";
|
|
24
|
-
import { OpenAPIAnalyzer } from "./analyzer-
|
|
24
|
+
import { OpenAPIAnalyzer } from "./analyzer-BkNQHWj4.js";
|
|
25
|
+
import { Readable } from "node:stream";
|
|
25
26
|
import * as zlib from "node:zlib";
|
|
26
27
|
import Ajv from "ajv";
|
|
27
28
|
import addFormats from "ajv-formats";
|
|
28
29
|
import { randomUUID, createHmac } from "crypto";
|
|
29
30
|
import { EventEmitter } from "events";
|
|
31
|
+
class BodyParser {
|
|
32
|
+
/**
|
|
33
|
+
* Parses the body of a request based on Content-Type header.
|
|
34
|
+
* @param req The ShokupanRequest object
|
|
35
|
+
* @param config Application configuration for limits and parser options
|
|
36
|
+
* @returns The parsed body or throws an error
|
|
37
|
+
*/
|
|
38
|
+
static async parse(req, config = {}) {
|
|
39
|
+
const contentType = req.headers.get("content-type") || "";
|
|
40
|
+
const maxBodySize = config.maxBodySize ?? 10 * 1024 * 1024;
|
|
41
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
42
|
+
return {
|
|
43
|
+
type: "json",
|
|
44
|
+
body: await BodyParser.parseJson(req, config.jsonParser || "native", maxBodySize)
|
|
45
|
+
};
|
|
46
|
+
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
47
|
+
return {
|
|
48
|
+
type: "formData",
|
|
49
|
+
body: await BodyParser.parseFormData(req, maxBodySize)
|
|
50
|
+
};
|
|
51
|
+
} else {
|
|
52
|
+
return {
|
|
53
|
+
type: "text",
|
|
54
|
+
body: await BodyParser.readRawBody(req, maxBodySize)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parsing helper for JSON
|
|
60
|
+
*/
|
|
61
|
+
static async parseJson(req, parserType, maxBodySize) {
|
|
62
|
+
const rawText = await BodyParser.readRawBody(req, maxBodySize);
|
|
63
|
+
if (parserType === "native") {
|
|
64
|
+
if (!rawText) return {};
|
|
65
|
+
return JSON.parse(rawText);
|
|
66
|
+
} else {
|
|
67
|
+
const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
|
|
68
|
+
const parser = getJSONParser(parserType);
|
|
69
|
+
return parser(rawText);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parsing helper for FormData
|
|
74
|
+
*/
|
|
75
|
+
static async parseFormData(req, maxBodySize) {
|
|
76
|
+
const clHeader = req.headers.get("content-length");
|
|
77
|
+
if (!clHeader) {
|
|
78
|
+
const err = new Error("Length Required");
|
|
79
|
+
err.status = 411;
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
const cl = parseInt(clHeader, 10);
|
|
83
|
+
if (isNaN(cl)) {
|
|
84
|
+
const err = new Error("Bad Request");
|
|
85
|
+
err.status = 400;
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
if (cl > maxBodySize) {
|
|
89
|
+
const err = new Error("Payload Too Large");
|
|
90
|
+
err.status = 413;
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
return req.formData();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Reads raw body as string with size enforcement
|
|
97
|
+
*/
|
|
98
|
+
static async readRawBody(req, maxBodySize) {
|
|
99
|
+
if (typeof req.body === "string") {
|
|
100
|
+
const body = req.body;
|
|
101
|
+
if (body.length > maxBodySize) {
|
|
102
|
+
const err = new Error("Payload Too Large");
|
|
103
|
+
err.status = 413;
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
return body;
|
|
107
|
+
}
|
|
108
|
+
const reader = req.body?.getReader();
|
|
109
|
+
if (!reader) {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
const chunks = [];
|
|
113
|
+
let totalSize = 0;
|
|
114
|
+
try {
|
|
115
|
+
while (true) {
|
|
116
|
+
const { done, value } = await reader.read();
|
|
117
|
+
if (done) break;
|
|
118
|
+
totalSize += value.length;
|
|
119
|
+
if (totalSize > maxBodySize) {
|
|
120
|
+
const err = new Error("Payload Too Large");
|
|
121
|
+
err.status = 413;
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
chunks.push(value);
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
reader.releaseLock();
|
|
128
|
+
}
|
|
129
|
+
const result = new Uint8Array(totalSize);
|
|
130
|
+
let offset = 0;
|
|
131
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
132
|
+
const chunk = chunks[i];
|
|
133
|
+
result.set(chunk, offset);
|
|
134
|
+
offset += chunk.length;
|
|
135
|
+
}
|
|
136
|
+
return new TextDecoder().decode(result);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
30
139
|
const HTTP_STATUS = {
|
|
31
140
|
// 2xx Success
|
|
32
141
|
OK: 200,
|
|
@@ -217,6 +326,7 @@ const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol"
|
|
|
217
326
|
const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
|
|
218
327
|
const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
|
|
219
328
|
const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
|
|
329
|
+
const $cachedCookies = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedCookies");
|
|
220
330
|
const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
|
|
221
331
|
const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
|
|
222
332
|
const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
|
|
@@ -275,6 +385,7 @@ class ShokupanContext {
|
|
|
275
385
|
[$cachedHost];
|
|
276
386
|
[$cachedOrigin];
|
|
277
387
|
[$cachedQuery];
|
|
388
|
+
[$cachedCookies];
|
|
278
389
|
disconnectCallbacks = [];
|
|
279
390
|
/**
|
|
280
391
|
* Registers a callback to be executed when the associated WebSocket disconnects.
|
|
@@ -372,13 +483,20 @@ class ShokupanContext {
|
|
|
372
483
|
if (this[$cachedQuery]) return this[$cachedQuery];
|
|
373
484
|
const q = /* @__PURE__ */ Object.create(null);
|
|
374
485
|
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
486
|
+
const mode = this.app?.applicationConfig?.queryParserMode || "extended";
|
|
375
487
|
this.url.searchParams.forEach((value, key) => {
|
|
376
488
|
if (blocklist.includes(key)) return;
|
|
377
489
|
if (Object.prototype.hasOwnProperty.call(q, key)) {
|
|
378
|
-
if (
|
|
379
|
-
|
|
490
|
+
if (mode === "strict") {
|
|
491
|
+
throw new Error(`Duplicate query parameter '${key}' is not allowed in strict mode.`);
|
|
492
|
+
} else if (mode === "simple") {
|
|
493
|
+
q[key] = value;
|
|
380
494
|
} else {
|
|
381
|
-
|
|
495
|
+
if (Array.isArray(q[key])) {
|
|
496
|
+
q[key].push(value);
|
|
497
|
+
} else {
|
|
498
|
+
q[key] = [q[key], value];
|
|
499
|
+
}
|
|
382
500
|
}
|
|
383
501
|
} else {
|
|
384
502
|
q[key] = value;
|
|
@@ -387,6 +505,28 @@ class ShokupanContext {
|
|
|
387
505
|
this[$cachedQuery] = q;
|
|
388
506
|
return q;
|
|
389
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Request cookies
|
|
510
|
+
*/
|
|
511
|
+
get cookies() {
|
|
512
|
+
if (this[$cachedCookies]) return this[$cachedCookies];
|
|
513
|
+
const c = /* @__PURE__ */ Object.create(null);
|
|
514
|
+
const cookieHeader = this.request.headers.get("cookie");
|
|
515
|
+
if (cookieHeader) {
|
|
516
|
+
const pairs = cookieHeader.split(";");
|
|
517
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
518
|
+
const pair = pairs[i];
|
|
519
|
+
const index = pair.indexOf("=");
|
|
520
|
+
if (index > 0) {
|
|
521
|
+
const key = pair.slice(0, index).trim();
|
|
522
|
+
const value = pair.slice(index + 1).trim();
|
|
523
|
+
c[key] = decodeURIComponent(value);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
this[$cachedCookies] = c;
|
|
528
|
+
return c;
|
|
529
|
+
}
|
|
390
530
|
/**
|
|
391
531
|
* Client IP address
|
|
392
532
|
*/
|
|
@@ -561,6 +701,10 @@ class ShokupanContext {
|
|
|
561
701
|
}
|
|
562
702
|
return h;
|
|
563
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Read request body with caching to avoid double parsing.
|
|
706
|
+
* The body is only parsed once and cached for subsequent reads.
|
|
707
|
+
*/
|
|
564
708
|
/**
|
|
565
709
|
* Read request body with caching to avoid double parsing.
|
|
566
710
|
* The body is only parsed once and cached for subsequent reads.
|
|
@@ -572,29 +716,10 @@ class ShokupanContext {
|
|
|
572
716
|
if (this[$bodyParsed] === true) {
|
|
573
717
|
return this[$cachedBody];
|
|
574
718
|
}
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
try {
|
|
580
|
-
this[$cachedBody] = await this.request.json();
|
|
581
|
-
} catch (e) {
|
|
582
|
-
throw e;
|
|
583
|
-
}
|
|
584
|
-
} else {
|
|
585
|
-
const rawText = await this.request.text();
|
|
586
|
-
const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
|
|
587
|
-
const parser = getJSONParser(parserType);
|
|
588
|
-
this[$cachedBody] = parser(rawText);
|
|
589
|
-
}
|
|
590
|
-
this[$bodyType] = "json";
|
|
591
|
-
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
592
|
-
this[$cachedBody] = await this.request.formData();
|
|
593
|
-
this[$bodyType] = "formData";
|
|
594
|
-
} else {
|
|
595
|
-
this[$cachedBody] = await this.request.text();
|
|
596
|
-
this[$bodyType] = "text";
|
|
597
|
-
}
|
|
719
|
+
const config = this.app?.applicationConfig || {};
|
|
720
|
+
const { type, body } = await BodyParser.parse(this.request, config);
|
|
721
|
+
this[$bodyType] = type;
|
|
722
|
+
this[$cachedBody] = body;
|
|
598
723
|
this[$bodyParsed] = true;
|
|
599
724
|
return this[$cachedBody];
|
|
600
725
|
}
|
|
@@ -610,45 +735,22 @@ class ShokupanContext {
|
|
|
610
735
|
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
611
736
|
return;
|
|
612
737
|
}
|
|
738
|
+
const maxBodySize = this.app?.applicationConfig?.maxBodySize ?? 10 * 1024 * 1024;
|
|
739
|
+
const contentLength = parseInt(this.request.headers.get("content-length") || "0", 10);
|
|
740
|
+
if (contentLength > maxBodySize) {
|
|
741
|
+
this[$bodyParseError] = new Error("Payload Too Large");
|
|
742
|
+
this[$bodyParseError].status = 413;
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
613
745
|
try {
|
|
614
746
|
await this.body();
|
|
615
747
|
} catch (error) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
* Read raw body from ReadableStream efficiently.
|
|
621
|
-
* This is much faster than request.text() for large payloads.
|
|
622
|
-
* Also handles the case where body is already a string (e.g., in tests).
|
|
623
|
-
*/
|
|
624
|
-
async readRawBody() {
|
|
625
|
-
if (typeof this.request.body === "string") {
|
|
626
|
-
return this.request.body;
|
|
627
|
-
}
|
|
628
|
-
const reader = this.request.body?.getReader();
|
|
629
|
-
if (!reader) {
|
|
630
|
-
return "";
|
|
631
|
-
}
|
|
632
|
-
const chunks = [];
|
|
633
|
-
let totalSize = 0;
|
|
634
|
-
try {
|
|
635
|
-
while (true) {
|
|
636
|
-
const { done, value } = await reader.read();
|
|
637
|
-
if (done) break;
|
|
638
|
-
chunks.push(value);
|
|
639
|
-
totalSize += value.length;
|
|
748
|
+
if (error.status === 413 || error.message === "Payload Too Large") {
|
|
749
|
+
this[$bodyParseError] = error;
|
|
750
|
+
} else {
|
|
751
|
+
this[$bodyParseError] = error;
|
|
640
752
|
}
|
|
641
|
-
} finally {
|
|
642
|
-
reader.releaseLock();
|
|
643
753
|
}
|
|
644
|
-
const result = new Uint8Array(totalSize);
|
|
645
|
-
let offset = 0;
|
|
646
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
647
|
-
const chunk = chunks[i];
|
|
648
|
-
result.set(chunk, offset);
|
|
649
|
-
offset += chunk.length;
|
|
650
|
-
}
|
|
651
|
-
return new TextDecoder().decode(result);
|
|
652
754
|
}
|
|
653
755
|
/**
|
|
654
756
|
* Send a response
|
|
@@ -748,7 +850,15 @@ class ShokupanContext {
|
|
|
748
850
|
}
|
|
749
851
|
this.response.status = status;
|
|
750
852
|
const finalHeaders = this.mergeHeaders();
|
|
751
|
-
|
|
853
|
+
const targetUrl = url instanceof Promise ? await url : url;
|
|
854
|
+
if (targetUrl.startsWith("//")) {
|
|
855
|
+
throw new Error("Invalid redirect: Protocol-relative URLs are not allowed.");
|
|
856
|
+
}
|
|
857
|
+
const lowerUrl = targetUrl.toLowerCase();
|
|
858
|
+
if (lowerUrl.startsWith("javascript:") || lowerUrl.startsWith("data:") || lowerUrl.startsWith("vbscript:")) {
|
|
859
|
+
throw new Error(`Invalid redirect: Unsafe protocol '${targetUrl.split(":")[0]}'`);
|
|
860
|
+
}
|
|
861
|
+
finalHeaders.set("Location", targetUrl);
|
|
752
862
|
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
753
863
|
return this[$finalResponse];
|
|
754
864
|
}
|
|
@@ -806,6 +916,185 @@ class ShokupanContext {
|
|
|
806
916
|
const html = await this.renderer(element, args);
|
|
807
917
|
return this.html(html, status, headers);
|
|
808
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* Pipe a ReadableStream to the response
|
|
921
|
+
* @param stream ReadableStream to pipe
|
|
922
|
+
* @param options Response options (status, headers)
|
|
923
|
+
*/
|
|
924
|
+
pipe(stream, options) {
|
|
925
|
+
const headers = this.mergeHeaders(options?.headers);
|
|
926
|
+
const status = options?.status ?? this.response.status ?? 200;
|
|
927
|
+
if (this.app?.applicationConfig?.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
928
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
929
|
+
}
|
|
930
|
+
this[$finalResponse] = new Response(stream, { status, headers });
|
|
931
|
+
return this[$finalResponse];
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Internal helper to create a streaming response with common infrastructure
|
|
935
|
+
* @private
|
|
936
|
+
*/
|
|
937
|
+
createStreamHelper(helperFactory, callback, onError, headers) {
|
|
938
|
+
let controller;
|
|
939
|
+
const aborted = { value: false };
|
|
940
|
+
const abortCallbacks = [];
|
|
941
|
+
const encoder = new TextEncoder();
|
|
942
|
+
let helper;
|
|
943
|
+
const stream = new ReadableStream({
|
|
944
|
+
start(ctrl) {
|
|
945
|
+
controller = ctrl;
|
|
946
|
+
helper = helperFactory(controller, aborted, abortCallbacks, encoder);
|
|
947
|
+
(async () => {
|
|
948
|
+
try {
|
|
949
|
+
await callback(helper);
|
|
950
|
+
controller.close();
|
|
951
|
+
} catch (err) {
|
|
952
|
+
if (onError) {
|
|
953
|
+
try {
|
|
954
|
+
await onError(err, helper);
|
|
955
|
+
} catch (handlerErr) {
|
|
956
|
+
console.error("Error in stream error handler:", handlerErr);
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
console.error("Stream error:", err);
|
|
960
|
+
}
|
|
961
|
+
if (!aborted.value) {
|
|
962
|
+
controller.close();
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
})();
|
|
966
|
+
},
|
|
967
|
+
async pull() {
|
|
968
|
+
},
|
|
969
|
+
cancel() {
|
|
970
|
+
aborted.value = true;
|
|
971
|
+
abortCallbacks.forEach((cb) => {
|
|
972
|
+
try {
|
|
973
|
+
cb();
|
|
974
|
+
} catch (err) {
|
|
975
|
+
console.error("Error in abort callback:", err);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
return this.pipe(stream, { headers });
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Generic streaming helper for binary/text data
|
|
984
|
+
* @param callback Callback function that receives a StreamHelper
|
|
985
|
+
* @param onError Optional error handler
|
|
986
|
+
*/
|
|
987
|
+
stream(callback, onError) {
|
|
988
|
+
return this.createStreamHelper(
|
|
989
|
+
(controller, aborted, abortCallbacks, encoder) => ({
|
|
990
|
+
async write(data) {
|
|
991
|
+
if (aborted.value) return;
|
|
992
|
+
const chunk = typeof data === "string" ? encoder.encode(data) : data;
|
|
993
|
+
controller.enqueue(chunk);
|
|
994
|
+
},
|
|
995
|
+
async pipe(stream) {
|
|
996
|
+
if (aborted.value) return;
|
|
997
|
+
const reader = stream.getReader();
|
|
998
|
+
try {
|
|
999
|
+
while (true) {
|
|
1000
|
+
const { done, value } = await reader.read();
|
|
1001
|
+
if (done || aborted.value) break;
|
|
1002
|
+
controller.enqueue(value);
|
|
1003
|
+
}
|
|
1004
|
+
} finally {
|
|
1005
|
+
reader.releaseLock();
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
sleep(ms) {
|
|
1009
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1010
|
+
},
|
|
1011
|
+
onAbort(callback2) {
|
|
1012
|
+
abortCallbacks.push(callback2);
|
|
1013
|
+
}
|
|
1014
|
+
}),
|
|
1015
|
+
callback,
|
|
1016
|
+
onError
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Text streaming helper with proper headers
|
|
1021
|
+
* @param callback Callback function that receives a TextStreamHelper
|
|
1022
|
+
* @param onError Optional error handler
|
|
1023
|
+
*/
|
|
1024
|
+
streamText(callback, onError) {
|
|
1025
|
+
const headers = new Headers(this.response.headers);
|
|
1026
|
+
headers.set("Content-Type", "text/plain; charset=utf-8");
|
|
1027
|
+
headers.set("Transfer-Encoding", "chunked");
|
|
1028
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
1029
|
+
return this.createStreamHelper(
|
|
1030
|
+
(controller, aborted, abortCallbacks, encoder) => ({
|
|
1031
|
+
async write(text) {
|
|
1032
|
+
if (aborted.value) return;
|
|
1033
|
+
controller.enqueue(encoder.encode(text));
|
|
1034
|
+
},
|
|
1035
|
+
async writeln(text) {
|
|
1036
|
+
if (aborted.value) return;
|
|
1037
|
+
controller.enqueue(encoder.encode(text + "\n"));
|
|
1038
|
+
},
|
|
1039
|
+
sleep(ms) {
|
|
1040
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1041
|
+
},
|
|
1042
|
+
onAbort(callback2) {
|
|
1043
|
+
abortCallbacks.push(callback2);
|
|
1044
|
+
}
|
|
1045
|
+
}),
|
|
1046
|
+
callback,
|
|
1047
|
+
onError,
|
|
1048
|
+
headers
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Server-Sent Events (SSE) streaming helper
|
|
1053
|
+
* @param callback Callback function that receives an SSEStreamHelper
|
|
1054
|
+
* @param onError Optional error handler
|
|
1055
|
+
*/
|
|
1056
|
+
streamSSE(callback, onError) {
|
|
1057
|
+
const headers = new Headers(this.response.headers);
|
|
1058
|
+
headers.set("Content-Type", "text/event-stream");
|
|
1059
|
+
headers.set("Cache-Control", "no-cache");
|
|
1060
|
+
headers.set("Connection", "keep-alive");
|
|
1061
|
+
return this.createStreamHelper(
|
|
1062
|
+
(controller, aborted, abortCallbacks, encoder) => ({
|
|
1063
|
+
async writeSSE(message) {
|
|
1064
|
+
if (aborted.value) return;
|
|
1065
|
+
let sseMessage = "";
|
|
1066
|
+
if (message.event) {
|
|
1067
|
+
sseMessage += `event: ${message.event}
|
|
1068
|
+
`;
|
|
1069
|
+
}
|
|
1070
|
+
if (message.id !== void 0) {
|
|
1071
|
+
sseMessage += `id: ${message.id}
|
|
1072
|
+
`;
|
|
1073
|
+
}
|
|
1074
|
+
if (message.retry !== void 0) {
|
|
1075
|
+
sseMessage += `retry: ${message.retry}
|
|
1076
|
+
`;
|
|
1077
|
+
}
|
|
1078
|
+
const dataLines = message.data.split("\n");
|
|
1079
|
+
for (const line of dataLines) {
|
|
1080
|
+
sseMessage += `data: ${line}
|
|
1081
|
+
`;
|
|
1082
|
+
}
|
|
1083
|
+
sseMessage += "\n";
|
|
1084
|
+
controller.enqueue(encoder.encode(sseMessage));
|
|
1085
|
+
},
|
|
1086
|
+
sleep(ms) {
|
|
1087
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1088
|
+
},
|
|
1089
|
+
onAbort(callback2) {
|
|
1090
|
+
abortCallbacks.push(callback2);
|
|
1091
|
+
}
|
|
1092
|
+
}),
|
|
1093
|
+
callback,
|
|
1094
|
+
onError,
|
|
1095
|
+
headers
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
809
1098
|
}
|
|
810
1099
|
const compose = (middleware) => {
|
|
811
1100
|
if (!middleware.length) {
|
|
@@ -822,6 +1111,11 @@ const compose = (middleware) => {
|
|
|
822
1111
|
return next ? next() : Promise.resolve();
|
|
823
1112
|
}
|
|
824
1113
|
const fn = middleware[i];
|
|
1114
|
+
if (typeof fn !== "function") {
|
|
1115
|
+
const name = fn?.constructor?.name;
|
|
1116
|
+
console.error(`[Middleware Error] Item at index ${i} is not a function! It is: ${typeof fn} (${name})`, fn);
|
|
1117
|
+
throw new TypeError(`Middleware at index ${i} must be a function, got ${name}`);
|
|
1118
|
+
}
|
|
825
1119
|
if (!context2[$debug]) {
|
|
826
1120
|
return fn(context2, () => runner(i + 1));
|
|
827
1121
|
}
|
|
@@ -1111,7 +1405,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1111
1405
|
let astMiddlewareRegistry = {};
|
|
1112
1406
|
let applications = [];
|
|
1113
1407
|
try {
|
|
1114
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-
|
|
1408
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BkNQHWj4.js");
|
|
1115
1409
|
const entrypoint = rootRouter.metadata?.file;
|
|
1116
1410
|
const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
|
|
1117
1411
|
const analysisResult = await analyzer.analyze();
|
|
@@ -1654,8 +1948,11 @@ function serveStatic(config, prefix) {
|
|
|
1654
1948
|
if (typeof Bun !== "undefined") {
|
|
1655
1949
|
response = new Response(Bun.file(finalPath));
|
|
1656
1950
|
} else {
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1951
|
+
const { createReadStream } = await import("node:fs");
|
|
1952
|
+
const { Readable: Readable2 } = await import("node:stream");
|
|
1953
|
+
const fileStream = createReadStream(finalPath);
|
|
1954
|
+
const webStream = Readable2.toWeb(fileStream);
|
|
1955
|
+
response = new Response(webStream);
|
|
1659
1956
|
}
|
|
1660
1957
|
if (config.hooks?.onResponse) {
|
|
1661
1958
|
const hooked = await config.hooks.onResponse(ctx, response);
|
|
@@ -1667,6 +1964,37 @@ function serveStatic(config, prefix) {
|
|
|
1667
1964
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1668
1965
|
return serveStaticMiddleware;
|
|
1669
1966
|
}
|
|
1967
|
+
const metadataStore = /* @__PURE__ */ new WeakMap();
|
|
1968
|
+
function defineMetadata(key, value, target, propertyKey) {
|
|
1969
|
+
let targetMetadata = metadataStore.get(target);
|
|
1970
|
+
if (!targetMetadata) {
|
|
1971
|
+
targetMetadata = /* @__PURE__ */ new Map();
|
|
1972
|
+
metadataStore.set(target, targetMetadata);
|
|
1973
|
+
}
|
|
1974
|
+
const storageKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : key;
|
|
1975
|
+
targetMetadata.set(storageKey, value);
|
|
1976
|
+
}
|
|
1977
|
+
function getMetadata(key, target, propertyKey) {
|
|
1978
|
+
const targetMetadata = metadataStore.get(target);
|
|
1979
|
+
if (!targetMetadata) return void 0;
|
|
1980
|
+
const storageKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : key;
|
|
1981
|
+
return targetMetadata.get(storageKey);
|
|
1982
|
+
}
|
|
1983
|
+
if (typeof Reflect === "object") {
|
|
1984
|
+
if (!Reflect.defineMetadata) {
|
|
1985
|
+
Reflect.defineMetadata = defineMetadata;
|
|
1986
|
+
}
|
|
1987
|
+
if (!Reflect.getMetadata) {
|
|
1988
|
+
Reflect.getMetadata = getMetadata;
|
|
1989
|
+
}
|
|
1990
|
+
if (!Reflect.metadata) {
|
|
1991
|
+
Reflect.metadata = function(metadataKey, metadataValue) {
|
|
1992
|
+
return function decorator(target, propertyKey) {
|
|
1993
|
+
defineMetadata(metadataKey, metadataValue, target, propertyKey);
|
|
1994
|
+
};
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1670
1998
|
class Container {
|
|
1671
1999
|
static services = /* @__PURE__ */ new Map();
|
|
1672
2000
|
static register(target, instance) {
|
|
@@ -1678,28 +2006,60 @@ class Container {
|
|
|
1678
2006
|
static has(target) {
|
|
1679
2007
|
return this.services.has(target);
|
|
1680
2008
|
}
|
|
2009
|
+
static cache = /* @__PURE__ */ new Map();
|
|
2010
|
+
static resolvingStack = /* @__PURE__ */ new Set();
|
|
1681
2011
|
static resolve(target) {
|
|
1682
2012
|
if (this.services.has(target)) {
|
|
1683
2013
|
return this.services.get(target);
|
|
1684
2014
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2015
|
+
if (this.resolvingStack.has(target)) {
|
|
2016
|
+
const cycle = Array.from(this.resolvingStack);
|
|
2017
|
+
cycle.push(target);
|
|
2018
|
+
throw new Error(`Circular dependency detected: ${cycle.map((t) => t.name || t).join(" -> ")}`);
|
|
2019
|
+
}
|
|
2020
|
+
this.resolvingStack.add(target);
|
|
2021
|
+
try {
|
|
2022
|
+
let meta = this.cache.get(target);
|
|
2023
|
+
if (!meta) {
|
|
2024
|
+
const scope = Reflect.getMetadata("di:scope", target) || "singleton";
|
|
2025
|
+
const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
|
|
2026
|
+
const manualTokens = Reflect.getMetadata("di:constructor:params", target) || [];
|
|
2027
|
+
const dependencies = paramTypes.map((param, index) => {
|
|
2028
|
+
const manual = manualTokens.find((t) => t.index === index);
|
|
2029
|
+
if (manual && manual.token) return manual.token;
|
|
2030
|
+
if (param === String || param === Number || param === Boolean || param === Object || param === void 0) return void 0;
|
|
2031
|
+
return param;
|
|
2032
|
+
});
|
|
2033
|
+
meta = { scope, dependencies };
|
|
2034
|
+
this.cache.set(target, meta);
|
|
2035
|
+
}
|
|
2036
|
+
const args = meta.dependencies.map((dep) => dep ? Container.resolve(dep) : void 0);
|
|
2037
|
+
const instance = new target(...args);
|
|
2038
|
+
if (typeof instance.onInit === "function") {
|
|
2039
|
+
instance.onInit();
|
|
2040
|
+
}
|
|
2041
|
+
if (meta.scope === "singleton") {
|
|
2042
|
+
this.services.set(target, instance);
|
|
2043
|
+
}
|
|
2044
|
+
return instance;
|
|
2045
|
+
} finally {
|
|
2046
|
+
this.resolvingStack.delete(target);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
static async teardown() {
|
|
2050
|
+
for (const [target, instance] of this.services.entries()) {
|
|
2051
|
+
if (typeof instance.onDestroy === "function") {
|
|
2052
|
+
await instance.onDestroy();
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
this.services.clear();
|
|
2056
|
+
this.cache.clear();
|
|
1688
2057
|
}
|
|
1689
2058
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
}
|
|
1694
|
-
function Inject(token) {
|
|
1695
|
-
return (target, key) => {
|
|
1696
|
-
Object.defineProperty(target, key, {
|
|
1697
|
-
get: () => Container.resolve(token),
|
|
1698
|
-
enumerable: true,
|
|
1699
|
-
configurable: true
|
|
1700
|
-
});
|
|
1701
|
-
};
|
|
1702
|
-
}
|
|
2059
|
+
const di = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
2060
|
+
__proto__: null,
|
|
2061
|
+
Container
|
|
2062
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1703
2063
|
const tracer = trace.getTracer("shokupan.middleware");
|
|
1704
2064
|
function traceHandler(fn, name) {
|
|
1705
2065
|
return async function(...args) {
|
|
@@ -1762,6 +2122,7 @@ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
|
1762
2122
|
RouteParamType2["HEADER"] = "HEADER";
|
|
1763
2123
|
RouteParamType2["REQUEST"] = "REQUEST";
|
|
1764
2124
|
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
2125
|
+
RouteParamType2["SERVICE"] = "SERVICE";
|
|
1765
2126
|
return RouteParamType2;
|
|
1766
2127
|
})(RouteParamType || {});
|
|
1767
2128
|
class ControllerScanner {
|
|
@@ -1793,7 +2154,7 @@ class ControllerScanner {
|
|
|
1793
2154
|
line: info.line,
|
|
1794
2155
|
name: instance.constructor.name
|
|
1795
2156
|
};
|
|
1796
|
-
router.
|
|
2157
|
+
router.bindController(instance);
|
|
1797
2158
|
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1798
2159
|
const proto = Object.getPrototypeOf(instance);
|
|
1799
2160
|
const methods = /* @__PURE__ */ new Set();
|
|
@@ -1915,6 +2276,9 @@ class ControllerScanner {
|
|
|
1915
2276
|
case RouteParamType.CONTEXT:
|
|
1916
2277
|
args[arg.index] = ctx;
|
|
1917
2278
|
break;
|
|
2279
|
+
case RouteParamType.SERVICE:
|
|
2280
|
+
args[arg.index] = Container.resolve(arg.token);
|
|
2281
|
+
break;
|
|
1918
2282
|
}
|
|
1919
2283
|
}
|
|
1920
2284
|
}
|
|
@@ -2104,6 +2468,15 @@ class ShokupanRequestBase {
|
|
|
2104
2468
|
this.headers = new Headers(this.headers);
|
|
2105
2469
|
}
|
|
2106
2470
|
}
|
|
2471
|
+
clone() {
|
|
2472
|
+
return new ShokupanRequest({
|
|
2473
|
+
method: this.method,
|
|
2474
|
+
url: this.url,
|
|
2475
|
+
headers: new Headers(this.headers),
|
|
2476
|
+
body: this.body
|
|
2477
|
+
// Shallow copy of body, might need deep copy if object
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2107
2480
|
}
|
|
2108
2481
|
const ShokupanRequest = ShokupanRequestBase;
|
|
2109
2482
|
class RouterTrie {
|
|
@@ -2269,9 +2642,6 @@ class ShokupanRouter {
|
|
|
2269
2642
|
return this._hasAfterValidateHook;
|
|
2270
2643
|
}
|
|
2271
2644
|
requestTimeout;
|
|
2272
|
-
get db() {
|
|
2273
|
-
return this.root?.db;
|
|
2274
|
-
}
|
|
2275
2645
|
hookCache = /* @__PURE__ */ new Map();
|
|
2276
2646
|
hooksInitialized = false;
|
|
2277
2647
|
middleware = [];
|
|
@@ -2297,7 +2667,7 @@ class ShokupanRouter {
|
|
|
2297
2667
|
return this;
|
|
2298
2668
|
}
|
|
2299
2669
|
// Registry Accessor
|
|
2300
|
-
|
|
2670
|
+
get registry() {
|
|
2301
2671
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
2302
2672
|
const localRoutes = [];
|
|
2303
2673
|
for (let i = 0; i < this[$routes].length; i++) {
|
|
@@ -2333,7 +2703,7 @@ class ShokupanRouter {
|
|
|
2333
2703
|
type: "router",
|
|
2334
2704
|
path: r[$mountPath].startsWith("/") ? r[$mountPath] : "/" + r[$mountPath],
|
|
2335
2705
|
metadata: r.metadata,
|
|
2336
|
-
children: r.
|
|
2706
|
+
children: r.registry
|
|
2337
2707
|
}));
|
|
2338
2708
|
const controllers = this[$childControllers].map((c) => {
|
|
2339
2709
|
const routes = controllerRoutesMap.get(c) || [];
|
|
@@ -2428,7 +2798,7 @@ class ShokupanRouter {
|
|
|
2428
2798
|
/**
|
|
2429
2799
|
* Registers a controller instance to the router.
|
|
2430
2800
|
*/
|
|
2431
|
-
|
|
2801
|
+
bindController(controller) {
|
|
2432
2802
|
this[$childControllers].push(controller);
|
|
2433
2803
|
}
|
|
2434
2804
|
/**
|
|
@@ -3005,68 +3375,6 @@ class ShokupanRouter {
|
|
|
3005
3375
|
}
|
|
3006
3376
|
}
|
|
3007
3377
|
}
|
|
3008
|
-
function createHttpServer() {
|
|
3009
|
-
return async (options) => {
|
|
3010
|
-
const server = http$1.createServer(async (req, res) => {
|
|
3011
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
3012
|
-
const request = new Request(url.toString(), {
|
|
3013
|
-
method: req.method,
|
|
3014
|
-
headers: req.headers,
|
|
3015
|
-
body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
|
|
3016
|
-
start(controller) {
|
|
3017
|
-
req.on("data", (chunk) => controller.enqueue(chunk));
|
|
3018
|
-
req.on("end", () => controller.close());
|
|
3019
|
-
req.on("error", (err) => controller.error(err));
|
|
3020
|
-
}
|
|
3021
|
-
}),
|
|
3022
|
-
// Required for Node.js undici when sending a body
|
|
3023
|
-
duplex: "half"
|
|
3024
|
-
});
|
|
3025
|
-
const response = await options.fetch(request, fauxServer);
|
|
3026
|
-
res.statusCode = response.status;
|
|
3027
|
-
response.headers.forEach((v, k) => res.setHeader(k, v));
|
|
3028
|
-
if (response.body) {
|
|
3029
|
-
const buffer = await response.arrayBuffer();
|
|
3030
|
-
res.end(Buffer.from(buffer));
|
|
3031
|
-
} else {
|
|
3032
|
-
res.end();
|
|
3033
|
-
}
|
|
3034
|
-
});
|
|
3035
|
-
const fauxServer = {
|
|
3036
|
-
stop: () => {
|
|
3037
|
-
server.close();
|
|
3038
|
-
return Promise.resolve();
|
|
3039
|
-
},
|
|
3040
|
-
upgrade(req, options2) {
|
|
3041
|
-
return false;
|
|
3042
|
-
},
|
|
3043
|
-
reload(options2) {
|
|
3044
|
-
return fauxServer;
|
|
3045
|
-
},
|
|
3046
|
-
get port() {
|
|
3047
|
-
const addr = server.address();
|
|
3048
|
-
if (typeof addr === "object" && addr !== null) {
|
|
3049
|
-
return addr.port;
|
|
3050
|
-
}
|
|
3051
|
-
return options.port;
|
|
3052
|
-
},
|
|
3053
|
-
hostname: options.hostname,
|
|
3054
|
-
development: options.development,
|
|
3055
|
-
pendingRequests: 0,
|
|
3056
|
-
requestIP: (req) => null,
|
|
3057
|
-
publish: () => 0,
|
|
3058
|
-
subscriberCount: () => 0,
|
|
3059
|
-
url: new URL(`http://${options.hostname}:${options.port}`),
|
|
3060
|
-
// Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)
|
|
3061
|
-
nodeServer: server
|
|
3062
|
-
};
|
|
3063
|
-
return new Promise((resolve2) => {
|
|
3064
|
-
server.listen(options.port, options.hostname, () => {
|
|
3065
|
-
resolve2(fauxServer);
|
|
3066
|
-
});
|
|
3067
|
-
});
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
3070
3378
|
class BunAdapter {
|
|
3071
3379
|
server;
|
|
3072
3380
|
async listen(port, app) {
|
|
@@ -3203,25 +3511,143 @@ class BunAdapter {
|
|
|
3203
3511
|
class NodeAdapter {
|
|
3204
3512
|
server;
|
|
3205
3513
|
async listen(port, app) {
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3514
|
+
const factory = app.applicationConfig.serverFactory;
|
|
3515
|
+
let nodeServer;
|
|
3516
|
+
if (factory) {
|
|
3517
|
+
const serveOptions = {
|
|
3518
|
+
port,
|
|
3519
|
+
hostname: app.applicationConfig.hostname,
|
|
3520
|
+
development: app.applicationConfig.development,
|
|
3521
|
+
fetch: app.fetch.bind(app),
|
|
3522
|
+
reusePort: app.applicationConfig.reusePort
|
|
3523
|
+
};
|
|
3524
|
+
this.server = await factory(serveOptions);
|
|
3525
|
+
return this.server;
|
|
3209
3526
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3527
|
+
nodeServer = http$1.createServer(async (req, res) => {
|
|
3528
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
3529
|
+
const request = new Request(url.toString(), {
|
|
3530
|
+
method: req.method,
|
|
3531
|
+
headers: req.headers,
|
|
3532
|
+
body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
|
|
3533
|
+
start(controller) {
|
|
3534
|
+
req.on("data", (chunk) => controller.enqueue(chunk));
|
|
3535
|
+
req.on("end", () => controller.close());
|
|
3536
|
+
req.on("error", (err) => controller.error(err));
|
|
3537
|
+
}
|
|
3538
|
+
}),
|
|
3539
|
+
// Required for Node.js undici when sending a body
|
|
3540
|
+
// @ts-ignore
|
|
3541
|
+
duplex: "half"
|
|
3542
|
+
});
|
|
3543
|
+
const response = await app.fetch(request, fauxServer);
|
|
3544
|
+
res.statusCode = response.status;
|
|
3545
|
+
response.headers.forEach((v, k) => res.setHeader(k, v));
|
|
3546
|
+
if (response.body) {
|
|
3547
|
+
const buffer = await response.arrayBuffer();
|
|
3548
|
+
res.end(Buffer.from(buffer));
|
|
3549
|
+
} else {
|
|
3550
|
+
res.end();
|
|
3551
|
+
}
|
|
3552
|
+
});
|
|
3553
|
+
this.server = nodeServer;
|
|
3554
|
+
const fauxServer = {
|
|
3555
|
+
stop: () => {
|
|
3556
|
+
nodeServer.close();
|
|
3557
|
+
return Promise.resolve();
|
|
3558
|
+
},
|
|
3559
|
+
upgrade(req, options) {
|
|
3560
|
+
return false;
|
|
3561
|
+
},
|
|
3562
|
+
reload(options) {
|
|
3563
|
+
return fauxServer;
|
|
3564
|
+
},
|
|
3565
|
+
get port() {
|
|
3566
|
+
const addr = nodeServer.address();
|
|
3567
|
+
if (typeof addr === "object" && addr !== null) {
|
|
3568
|
+
return addr.port;
|
|
3569
|
+
}
|
|
3570
|
+
return port;
|
|
3571
|
+
},
|
|
3572
|
+
hostname: app.applicationConfig.hostname || "localhost",
|
|
3573
|
+
development: app.applicationConfig.development || false,
|
|
3574
|
+
pendingRequests: 0,
|
|
3575
|
+
requestIP: (req) => null,
|
|
3576
|
+
publish: () => 0,
|
|
3577
|
+
subscriberCount: () => 0,
|
|
3578
|
+
url: new URL(`http://${app.applicationConfig.hostname || "localhost"}:${port}`),
|
|
3579
|
+
// Expose the raw Node.js server
|
|
3580
|
+
// @ts-ignore
|
|
3581
|
+
nodeServer
|
|
3217
3582
|
};
|
|
3218
|
-
|
|
3219
|
-
|
|
3583
|
+
return new Promise((resolve2) => {
|
|
3584
|
+
nodeServer.listen(port, app.applicationConfig.hostname, () => {
|
|
3585
|
+
resolve2(fauxServer);
|
|
3586
|
+
});
|
|
3587
|
+
});
|
|
3220
3588
|
}
|
|
3221
3589
|
async stop() {
|
|
3222
3590
|
if (this.server?.stop) {
|
|
3223
3591
|
await this.server.stop();
|
|
3592
|
+
} else if (this.server?.close) {
|
|
3593
|
+
this.server.close();
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
class ShokupanServer {
|
|
3598
|
+
constructor(app) {
|
|
3599
|
+
this.app = app;
|
|
3600
|
+
}
|
|
3601
|
+
server;
|
|
3602
|
+
adapter;
|
|
3603
|
+
/**
|
|
3604
|
+
* Starts the application server.
|
|
3605
|
+
* @param port The port to listen on.
|
|
3606
|
+
*/
|
|
3607
|
+
async listen(port) {
|
|
3608
|
+
const config = this.app.applicationConfig;
|
|
3609
|
+
const finalPort = port ?? config.port ?? 3e3;
|
|
3610
|
+
if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
|
|
3611
|
+
throw new Error("Invalid port number");
|
|
3612
|
+
}
|
|
3613
|
+
await this.app.start();
|
|
3614
|
+
let adapterName = config.adapter;
|
|
3615
|
+
let adapter;
|
|
3616
|
+
if (!adapterName) {
|
|
3617
|
+
if (typeof Bun !== "undefined") {
|
|
3618
|
+
config.adapter = "bun";
|
|
3619
|
+
adapter = new BunAdapter();
|
|
3620
|
+
} else {
|
|
3621
|
+
config.adapter = "node";
|
|
3622
|
+
adapter = new NodeAdapter();
|
|
3623
|
+
}
|
|
3624
|
+
} else if (adapterName === "bun") {
|
|
3625
|
+
adapter = new BunAdapter();
|
|
3626
|
+
} else if (adapterName === "node") {
|
|
3627
|
+
adapter = new NodeAdapter();
|
|
3628
|
+
} else if (adapterName === "wintercg") {
|
|
3629
|
+
throw new Error("WinterCG adapter does not support listen(). Use fetch directly.");
|
|
3630
|
+
} else {
|
|
3631
|
+
adapter = new NodeAdapter();
|
|
3632
|
+
}
|
|
3633
|
+
this.adapter = adapter;
|
|
3634
|
+
this.app.compile();
|
|
3635
|
+
this.server = await adapter.listen(finalPort, this.app);
|
|
3636
|
+
if (finalPort === 0 && this.server?.port) {
|
|
3637
|
+
config.port = this.server.port;
|
|
3224
3638
|
}
|
|
3639
|
+
return this.server;
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* Stops the server.
|
|
3643
|
+
*/
|
|
3644
|
+
async stop(closeActiveConnections) {
|
|
3645
|
+
if (this.adapter?.stop) {
|
|
3646
|
+
await this.adapter.stop();
|
|
3647
|
+
} else if (this.server?.stop) {
|
|
3648
|
+
await this.server.stop(closeActiveConnections);
|
|
3649
|
+
}
|
|
3650
|
+
this.server = void 0;
|
|
3225
3651
|
}
|
|
3226
3652
|
}
|
|
3227
3653
|
let fs;
|
|
@@ -3412,6 +3838,7 @@ const defaults = {
|
|
|
3412
3838
|
development: process.env.NODE_ENV !== "production",
|
|
3413
3839
|
enableAsyncLocalStorage: false,
|
|
3414
3840
|
enableHttpBridge: false,
|
|
3841
|
+
enableOpenApiGen: true,
|
|
3415
3842
|
reusePort: false
|
|
3416
3843
|
};
|
|
3417
3844
|
class Shokupan extends ShokupanRouter {
|
|
@@ -3423,8 +3850,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
3423
3850
|
composedMiddleware;
|
|
3424
3851
|
cpuMonitor;
|
|
3425
3852
|
server;
|
|
3853
|
+
httpServer;
|
|
3426
3854
|
datastore;
|
|
3427
3855
|
dbPromise;
|
|
3856
|
+
// Performance: Flattened Router Trie
|
|
3857
|
+
rootTrie;
|
|
3428
3858
|
get db() {
|
|
3429
3859
|
return this.datastore;
|
|
3430
3860
|
}
|
|
@@ -3445,6 +3875,10 @@ class Shokupan extends ShokupanRouter {
|
|
|
3445
3875
|
line,
|
|
3446
3876
|
name: "ShokupanApplication"
|
|
3447
3877
|
};
|
|
3878
|
+
if (this.applicationConfig.securityHeaders !== false) {
|
|
3879
|
+
const { SecurityHeaders: SecurityHeaders2 } = require("./plugins/middleware/security-headers");
|
|
3880
|
+
this.use(SecurityHeaders2(this.applicationConfig.securityHeaders === true ? {} : this.applicationConfig.securityHeaders));
|
|
3881
|
+
}
|
|
3448
3882
|
if (this.applicationConfig.adapter !== "wintercg") {
|
|
3449
3883
|
this.dbPromise = this.initDatastore().catch((err) => {
|
|
3450
3884
|
this.logger?.debug("Failed to initialize default datastore", { error: err });
|
|
@@ -3525,11 +3959,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
3525
3959
|
* @param port - The port to listen on. If not specified, the port from the configuration is used. If that is not specified, port 3000 is used.
|
|
3526
3960
|
* @returns The server instance.
|
|
3527
3961
|
*/
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3962
|
+
/**
|
|
3963
|
+
* Prepare the application for listening.
|
|
3964
|
+
* Use this if you want to initialize the app without starting the server immediately.
|
|
3965
|
+
*/
|
|
3966
|
+
async start() {
|
|
3533
3967
|
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
3534
3968
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
3535
3969
|
this.get("/.well-known/openapi.yaml", async (ctx) => {
|
|
@@ -3560,13 +3994,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
3560
3994
|
auth: config.auth || { type: "none" },
|
|
3561
3995
|
api: config.api || {
|
|
3562
3996
|
type: "openapi",
|
|
3563
|
-
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
3997
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/.well-known/openapi.yaml`,
|
|
3564
3998
|
is_user_authenticated: false
|
|
3565
3999
|
},
|
|
3566
|
-
logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
4000
|
+
logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/logo.png`,
|
|
3567
4001
|
// Placeholder default
|
|
3568
4002
|
contact_email: config.contact_email || pkg.author?.email || "support@example.com",
|
|
3569
|
-
legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
4003
|
+
legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/legal`
|
|
3570
4004
|
};
|
|
3571
4005
|
return ctx.json(manifest);
|
|
3572
4006
|
});
|
|
@@ -3579,8 +4013,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
3579
4013
|
versions: config.versions || [
|
|
3580
4014
|
{
|
|
3581
4015
|
name: this.openApiSpec.info.version || "v1",
|
|
3582
|
-
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
3583
|
-
spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
4016
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/`,
|
|
4017
|
+
spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/.well-known/openapi.yaml`
|
|
3584
4018
|
}
|
|
3585
4019
|
]
|
|
3586
4020
|
};
|
|
@@ -3616,28 +4050,20 @@ class Shokupan extends ShokupanRouter {
|
|
|
3616
4050
|
await this.asyncApiSpecPromise;
|
|
3617
4051
|
}
|
|
3618
4052
|
}
|
|
3619
|
-
if (port === 0 && process.platform === "linux") ;
|
|
3620
4053
|
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
3621
4054
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
3622
4055
|
this.cpuMonitor.start();
|
|
3623
4056
|
}
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
adapter = new BunAdapter();
|
|
3635
|
-
} else if (adapter === "node") {
|
|
3636
|
-
adapter = new NodeAdapter();
|
|
3637
|
-
} else if (adapter === "wintercg") {
|
|
3638
|
-
throw new Error("WinterCG adapter does not support listen(). Use fetch directly.");
|
|
3639
|
-
}
|
|
3640
|
-
this.server = await adapter.listen(finalPort, this);
|
|
4057
|
+
}
|
|
4058
|
+
/**
|
|
4059
|
+
* Starts the application server.
|
|
4060
|
+
*
|
|
4061
|
+
* @param port - The port to listen on. If not specified, the port from the configuration is used. If that is not specified, port 3000 is used.
|
|
4062
|
+
* @returns The server instance.
|
|
4063
|
+
*/
|
|
4064
|
+
async listen(port) {
|
|
4065
|
+
this.httpServer = new ShokupanServer(this);
|
|
4066
|
+
this.server = await this.httpServer.listen(port);
|
|
3641
4067
|
return this.server;
|
|
3642
4068
|
}
|
|
3643
4069
|
/**
|
|
@@ -3663,10 +4089,14 @@ class Shokupan extends ShokupanRouter {
|
|
|
3663
4089
|
this.cpuMonitor.stop();
|
|
3664
4090
|
this.cpuMonitor = void 0;
|
|
3665
4091
|
}
|
|
3666
|
-
if (this.
|
|
4092
|
+
if (this.httpServer !== void 0) {
|
|
4093
|
+
await this.httpServer.stop(closeActiveConnections);
|
|
4094
|
+
} else if (this.server?.stop) {
|
|
3667
4095
|
await this.server.stop(closeActiveConnections);
|
|
3668
|
-
this.server = void 0;
|
|
3669
4096
|
}
|
|
4097
|
+
this.server = void 0;
|
|
4098
|
+
const { Container: Container2 } = await Promise.resolve().then(() => di);
|
|
4099
|
+
await Container2.teardown();
|
|
3670
4100
|
}
|
|
3671
4101
|
[$dispatch](req) {
|
|
3672
4102
|
return this.fetch(req);
|
|
@@ -3675,6 +4105,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
3675
4105
|
* Processes a request by wrapping the standard fetch method.
|
|
3676
4106
|
*/
|
|
3677
4107
|
async testRequest(options) {
|
|
4108
|
+
if (!this.rootTrie) {
|
|
4109
|
+
this.compile();
|
|
4110
|
+
}
|
|
3678
4111
|
let url = options.url || options.path || "/";
|
|
3679
4112
|
if (!url.startsWith("http")) {
|
|
3680
4113
|
const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
|
|
@@ -3790,7 +4223,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
3790
4223
|
} else if (ctx.isUpgraded) {
|
|
3791
4224
|
return void 0;
|
|
3792
4225
|
} else if (ctx[$routeMatched]) {
|
|
3793
|
-
|
|
4226
|
+
let status = ctx.response.status;
|
|
4227
|
+
if (status === HTTP_STATUS.OK) {
|
|
4228
|
+
status = HTTP_STATUS.NO_CONTENT;
|
|
4229
|
+
}
|
|
4230
|
+
response = ctx.send(null, { status, headers: ctx.response.headers });
|
|
3794
4231
|
} else {
|
|
3795
4232
|
if (ctx.upgrade()) {
|
|
3796
4233
|
return void 0;
|
|
@@ -3819,8 +4256,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
3819
4256
|
if (err instanceof SyntaxError && err.message.includes("JSON")) {
|
|
3820
4257
|
status = 400;
|
|
3821
4258
|
}
|
|
3822
|
-
const
|
|
3823
|
-
|
|
4259
|
+
const isDev = this.applicationConfig.development !== false;
|
|
4260
|
+
const message = isDev ? err.message || "Internal Server Error" : "Internal Server Error";
|
|
4261
|
+
const body = { error: message };
|
|
4262
|
+
if (isDev && err.errors) body.errors = err.errors;
|
|
4263
|
+
if (isDev && err.stack) body.stack = err.stack;
|
|
3824
4264
|
if (this.hasOnErrorHook) await this.runHooks("onError", ctx, err);
|
|
3825
4265
|
return ctx.json(body, status);
|
|
3826
4266
|
}
|
|
@@ -3849,6 +4289,72 @@ class Shokupan extends ShokupanRouter {
|
|
|
3849
4289
|
return res;
|
|
3850
4290
|
});
|
|
3851
4291
|
}
|
|
4292
|
+
/**
|
|
4293
|
+
* Compiles all routes into a master Trie for O(1) router lookup.
|
|
4294
|
+
* Use this if adding routes dynamically after start (not recommended but possible).
|
|
4295
|
+
*/
|
|
4296
|
+
compile() {
|
|
4297
|
+
this.rootTrie = new RouterTrie();
|
|
4298
|
+
this.flattenRoutes(this.rootTrie, this, "", []);
|
|
4299
|
+
}
|
|
4300
|
+
flattenRoutes(trie, router, prefix, middlewareStack) {
|
|
4301
|
+
let effectiveStack = middlewareStack;
|
|
4302
|
+
if (router !== this) {
|
|
4303
|
+
effectiveStack = [...middlewareStack, ...router.middleware];
|
|
4304
|
+
}
|
|
4305
|
+
const joinPath = (base, segment) => {
|
|
4306
|
+
let b = base;
|
|
4307
|
+
if (b !== "/" && b.endsWith("/")) {
|
|
4308
|
+
b = b.slice(0, -1);
|
|
4309
|
+
}
|
|
4310
|
+
let s = segment;
|
|
4311
|
+
if (s === "/") {
|
|
4312
|
+
return b;
|
|
4313
|
+
}
|
|
4314
|
+
if (s === "") {
|
|
4315
|
+
return b;
|
|
4316
|
+
}
|
|
4317
|
+
if (!s.startsWith("/")) {
|
|
4318
|
+
s = "/" + s;
|
|
4319
|
+
}
|
|
4320
|
+
if (b === "/") {
|
|
4321
|
+
return s;
|
|
4322
|
+
}
|
|
4323
|
+
return b + s;
|
|
4324
|
+
};
|
|
4325
|
+
for (const route of router[$routes]) {
|
|
4326
|
+
const fullPath = joinPath(prefix, route.path);
|
|
4327
|
+
let handler = route.bakedHandler || route.handler;
|
|
4328
|
+
if (effectiveStack.length > 0) {
|
|
4329
|
+
const fn = compose(effectiveStack);
|
|
4330
|
+
const originalHandler = handler;
|
|
4331
|
+
handler = async (ctx) => {
|
|
4332
|
+
return fn(ctx, () => originalHandler(ctx));
|
|
4333
|
+
};
|
|
4334
|
+
handler.originalHandler = originalHandler.originalHandler || originalHandler;
|
|
4335
|
+
}
|
|
4336
|
+
trie.insert(route.method, fullPath, handler);
|
|
4337
|
+
if ((route.path === "/" || route.path === "") && fullPath !== "/") {
|
|
4338
|
+
trie.insert(route.method, fullPath + "/", handler);
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
for (const child of router[$childRouters]) {
|
|
4342
|
+
const mountPath = child[$mountPath];
|
|
4343
|
+
const childPrefix = joinPath(prefix, mountPath);
|
|
4344
|
+
this.flattenRoutes(trie, child, childPrefix, effectiveStack);
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
find(method, path) {
|
|
4348
|
+
if (this.rootTrie) {
|
|
4349
|
+
const result = this.rootTrie.search(method, path);
|
|
4350
|
+
if (result) return result;
|
|
4351
|
+
if (method === "HEAD") {
|
|
4352
|
+
return this.rootTrie.search("GET", path);
|
|
4353
|
+
}
|
|
4354
|
+
return null;
|
|
4355
|
+
}
|
|
4356
|
+
return super.find(method, path);
|
|
4357
|
+
}
|
|
3852
4358
|
}
|
|
3853
4359
|
function RateLimitMiddleware(options = {}) {
|
|
3854
4360
|
const windowMs = options.windowMs || 60 * 1e3;
|
|
@@ -3858,6 +4364,7 @@ function RateLimitMiddleware(options = {}) {
|
|
|
3858
4364
|
const headers = options.headers !== false;
|
|
3859
4365
|
const mode = options.mode || "user";
|
|
3860
4366
|
const trustedProxies = options.trustedProxies || [];
|
|
4367
|
+
const cleanupInterval = options.cleanupInterval || windowMs;
|
|
3861
4368
|
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
3862
4369
|
if (mode === "absolute") {
|
|
3863
4370
|
return "global";
|
|
@@ -3887,7 +4394,7 @@ function RateLimitMiddleware(options = {}) {
|
|
|
3887
4394
|
hits.delete(key);
|
|
3888
4395
|
}
|
|
3889
4396
|
}
|
|
3890
|
-
},
|
|
4397
|
+
}, cleanupInterval);
|
|
3891
4398
|
if (interval.unref) interval.unref();
|
|
3892
4399
|
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
3893
4400
|
if (skip(ctx)) return next();
|
|
@@ -3944,8 +4451,79 @@ function Controller(path = "/") {
|
|
|
3944
4451
|
target[$controllerPath] = path;
|
|
3945
4452
|
};
|
|
3946
4453
|
}
|
|
3947
|
-
function
|
|
3948
|
-
return (target
|
|
4454
|
+
function Injectable(scope = "singleton") {
|
|
4455
|
+
return (target) => {
|
|
4456
|
+
Reflect.defineMetadata("di:scope", scope, target);
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
function Inject(token) {
|
|
4460
|
+
return (target, propertyKey, indexOrDescriptor) => {
|
|
4461
|
+
if (typeof indexOrDescriptor === "undefined" || typeof indexOrDescriptor === "object" && indexOrDescriptor !== null) {
|
|
4462
|
+
const key = String(propertyKey);
|
|
4463
|
+
Object.defineProperty(target, key, {
|
|
4464
|
+
get: () => Container.resolve(token),
|
|
4465
|
+
enumerable: true,
|
|
4466
|
+
configurable: true
|
|
4467
|
+
});
|
|
4468
|
+
return;
|
|
4469
|
+
}
|
|
4470
|
+
if (typeof indexOrDescriptor === "number") {
|
|
4471
|
+
const index = indexOrDescriptor;
|
|
4472
|
+
const existing = Reflect.getMetadata("di:constructor:params", target) || [];
|
|
4473
|
+
existing.push({ index, token });
|
|
4474
|
+
Reflect.defineMetadata("di:constructor:params", existing, target);
|
|
4475
|
+
}
|
|
4476
|
+
};
|
|
4477
|
+
}
|
|
4478
|
+
function Use(tokenOrMiddleware, ...moreMiddleware) {
|
|
4479
|
+
return (target, propertyKey, indexOrDescriptor) => {
|
|
4480
|
+
if (typeof indexOrDescriptor === "number") {
|
|
4481
|
+
const index = indexOrDescriptor;
|
|
4482
|
+
if (!propertyKey) {
|
|
4483
|
+
let token2 = tokenOrMiddleware;
|
|
4484
|
+
if (!token2) {
|
|
4485
|
+
const paramTypes = Reflect.getMetadata("design:paramtypes", target);
|
|
4486
|
+
if (paramTypes && paramTypes[index]) {
|
|
4487
|
+
token2 = paramTypes[index];
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
const existing = Reflect.getMetadata("di:constructor:params", target) || [];
|
|
4491
|
+
existing.push({ index, token: token2 });
|
|
4492
|
+
Reflect.defineMetadata("di:constructor:params", existing, target);
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
if (!target[$routeArgs]) target[$routeArgs] = /* @__PURE__ */ new Map();
|
|
4496
|
+
if (!target[$routeArgs].has(propertyKey)) target[$routeArgs].set(propertyKey, []);
|
|
4497
|
+
let token = tokenOrMiddleware;
|
|
4498
|
+
if (!token) {
|
|
4499
|
+
const paramTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey);
|
|
4500
|
+
if (paramTypes && paramTypes[index]) {
|
|
4501
|
+
token = paramTypes[index];
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
target[$routeArgs].get(propertyKey).push({
|
|
4505
|
+
index,
|
|
4506
|
+
type: RouteParamType.SERVICE,
|
|
4507
|
+
token
|
|
4508
|
+
});
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4511
|
+
if (typeof propertyKey === "string" && indexOrDescriptor === void 0) {
|
|
4512
|
+
let token = tokenOrMiddleware;
|
|
4513
|
+
if (!token) {
|
|
4514
|
+
token = Reflect.getMetadata("design:type", target, propertyKey);
|
|
4515
|
+
}
|
|
4516
|
+
Object.defineProperty(target, propertyKey, {
|
|
4517
|
+
get: () => {
|
|
4518
|
+
if (!token) throw new Error(`Cannot resolve dependency for ${target.constructor.name}.${propertyKey} - no token provided and types unavailable.`);
|
|
4519
|
+
return Container.resolve(token);
|
|
4520
|
+
},
|
|
4521
|
+
enumerable: true,
|
|
4522
|
+
configurable: true
|
|
4523
|
+
});
|
|
4524
|
+
return;
|
|
4525
|
+
}
|
|
4526
|
+
const middleware = [tokenOrMiddleware, ...moreMiddleware];
|
|
3949
4527
|
if (!propertyKey) {
|
|
3950
4528
|
const existing = target[$middleware] || [];
|
|
3951
4529
|
target[$middleware] = [...existing, ...middleware];
|
|
@@ -4025,7 +4603,7 @@ function Event(eventName) {
|
|
|
4025
4603
|
function RateLimit(options) {
|
|
4026
4604
|
return Use(RateLimitMiddleware(options));
|
|
4027
4605
|
}
|
|
4028
|
-
function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
4606
|
+
function ApiExplorerApp({ spec, base, asyncSpec, config }) {
|
|
4029
4607
|
const hierarchy = /* @__PURE__ */ new Map();
|
|
4030
4608
|
const addRoute = (groupKey, route) => {
|
|
4031
4609
|
if (!hierarchy.has(groupKey)) {
|
|
@@ -4211,8 +4789,8 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
|
4211
4789
|
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
4212
4790
|
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
|
|
4213
4791
|
/* @__PURE__ */ jsx("link", { href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Vend+Sans:ital,wght@0,300..700;1,300..700&display=swap", rel: "stylesheet" }),
|
|
4214
|
-
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href:
|
|
4215
|
-
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href:
|
|
4792
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
|
|
4793
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
4216
4794
|
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
|
|
4217
4795
|
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
|
|
4218
4796
|
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
|
|
@@ -4232,7 +4810,7 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
|
4232
4810
|
/* @__PURE__ */ jsx(Sidebar$1, { spec, hierarchicalGroups }),
|
|
4233
4811
|
/* @__PURE__ */ jsx(MainContent$1, { allRoutes, config, spec })
|
|
4234
4812
|
] }),
|
|
4235
|
-
/* @__PURE__ */ jsx("script", { src:
|
|
4813
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/explorer-client.mjs`, type: "module" })
|
|
4236
4814
|
] })
|
|
4237
4815
|
] });
|
|
4238
4816
|
}
|
|
@@ -4418,8 +4996,14 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
4418
4996
|
this.get("/_source", async (ctx) => {
|
|
4419
4997
|
const file = ctx.query["file"];
|
|
4420
4998
|
if (!file) return ctx.text("Missing file parameter", 400);
|
|
4999
|
+
const { resolve: resolve2, normalize, isAbsolute } = await import("node:path");
|
|
5000
|
+
const cwd = process.cwd();
|
|
5001
|
+
const resolvedPath = resolve2(cwd, file);
|
|
5002
|
+
if (!resolvedPath.startsWith(cwd)) {
|
|
5003
|
+
return ctx.text("Forbidden: File must be within project root", 403);
|
|
5004
|
+
}
|
|
4421
5005
|
try {
|
|
4422
|
-
const content = await readFile$1(
|
|
5006
|
+
const content = await readFile$1(resolvedPath, "utf-8");
|
|
4423
5007
|
return ctx.text(content);
|
|
4424
5008
|
} catch (err) {
|
|
4425
5009
|
return ctx.text("File not found", 404);
|
|
@@ -4432,7 +5016,8 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
4432
5016
|
this.get("/", async (ctx) => {
|
|
4433
5017
|
const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
|
|
4434
5018
|
const asyncSpec = ctx.app.asyncApiSpec;
|
|
4435
|
-
const
|
|
5019
|
+
const base = this.pluginOptions.path;
|
|
5020
|
+
const element = ApiExplorerApp({ spec: stripSourceCode(spec), base, asyncSpec });
|
|
4436
5021
|
const html = renderToString(element);
|
|
4437
5022
|
if (html.length === 0) throw new Error("ApiExplorerPlugin: rendered page is blank.");
|
|
4438
5023
|
return ctx.html(html);
|
|
@@ -4474,12 +5059,12 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
|
4474
5059
|
] });
|
|
4475
5060
|
}
|
|
4476
5061
|
function Sidebar({ navTree, disableSourceView }) {
|
|
4477
|
-
return /* @__PURE__ */ jsxs("div", { class: "sidebar
|
|
5062
|
+
return /* @__PURE__ */ jsxs("div", { class: "sidebar", id: "sidebar", children: [
|
|
4478
5063
|
/* @__PURE__ */ jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
|
|
4479
5064
|
/* @__PURE__ */ jsx("h2", { children: "AsyncAPI" }),
|
|
4480
5065
|
/* @__PURE__ */ jsx("button", { id: "btn-collapse-nav", class: "btn-icon", title: "Collapse Sidebar", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) }) })
|
|
4481
5066
|
] }),
|
|
4482
|
-
/* @__PURE__ */ jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
|
|
5067
|
+
/* @__PURE__ */ jsx("div", { class: "nav-list scroller", id: "nav-list", children: /* @__PURE__ */ jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
|
|
4483
5068
|
] });
|
|
4484
5069
|
}
|
|
4485
5070
|
function NavNode({ node, level, disableSourceView }) {
|
|
@@ -4649,7 +5234,7 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4649
5234
|
let astMiddlewareRegistry = {};
|
|
4650
5235
|
let applications = [];
|
|
4651
5236
|
try {
|
|
4652
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-
|
|
5237
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BkNQHWj4.js");
|
|
4653
5238
|
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
4654
5239
|
const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
|
|
4655
5240
|
const analysisResult = await analyzer.analyze();
|
|
@@ -5059,8 +5644,14 @@ class AsyncApiPlugin extends ShokupanRouter {
|
|
|
5059
5644
|
if (!file || typeof file !== "string") {
|
|
5060
5645
|
return ctx.text("Missing file parameter", 400);
|
|
5061
5646
|
}
|
|
5647
|
+
const { resolve: resolve2 } = await import("node:path");
|
|
5648
|
+
const cwd = process.cwd();
|
|
5649
|
+
const resolvedPath = resolve2(cwd, file);
|
|
5650
|
+
if (!resolvedPath.startsWith(cwd)) {
|
|
5651
|
+
return ctx.text("Forbidden: File must be within project root", 403);
|
|
5652
|
+
}
|
|
5062
5653
|
try {
|
|
5063
|
-
const content = await readFile(
|
|
5654
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
5064
5655
|
return ctx.text(content);
|
|
5065
5656
|
} catch (e) {
|
|
5066
5657
|
return ctx.text("File not found: " + e.message, 404);
|
|
@@ -5115,7 +5706,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
5115
5706
|
}
|
|
5116
5707
|
}
|
|
5117
5708
|
async createSession(user, ctx) {
|
|
5118
|
-
const alg = "HS256";
|
|
5709
|
+
const alg = this.authConfig.jwtAlgorithm || "HS256";
|
|
5119
5710
|
const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
5120
5711
|
const opts = this.authConfig.cookieOptions || {};
|
|
5121
5712
|
let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
|
|
@@ -5417,7 +6008,7 @@ class ClusterPlugin {
|
|
|
5417
6008
|
}
|
|
5418
6009
|
}
|
|
5419
6010
|
}
|
|
5420
|
-
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern }) {
|
|
6011
|
+
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern, ignorePaths }) {
|
|
5421
6012
|
return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
|
|
5422
6013
|
/* @__PURE__ */ jsxs("head", { children: [
|
|
5423
6014
|
/* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
|
|
@@ -5527,22 +6118,22 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
|
|
|
5527
6118
|
/* @__PURE__ */ jsx("option", { value: "ws", children: "WS" }),
|
|
5528
6119
|
/* @__PURE__ */ jsx("option", { value: "other", children: "Other" })
|
|
5529
6120
|
] }),
|
|
6121
|
+
/* @__PURE__ */ jsxs("div", { style: "display: flex; align-items: center; gap: 4px; background: var(--bg-primary); padding: 0 8px; border: 1px solid var(--card-border); border-radius: 4px; color: var(--text-primary);", children: [
|
|
6122
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", id: "network-filter-ignore", checked: true }),
|
|
6123
|
+
/* @__PURE__ */ jsx("label", { for: "network-filter-ignore", style: "cursor: pointer; font-size: 0.9em; user-select: none;", children: "Excl. Ignored" })
|
|
6124
|
+
] }),
|
|
5530
6125
|
/* @__PURE__ */ jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Refresh" }),
|
|
5531
6126
|
/* @__PURE__ */ jsx("button", { onclick: "purgeRequests()", style: "background: var(--bg-primary); color: var(--color-error, #ef4444); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Purge" })
|
|
5532
6127
|
] }) }),
|
|
5533
|
-
/* @__PURE__ */ jsx("div", { id: "network-view", class: "active", style: "display: block; height:
|
|
6128
|
+
/* @__PURE__ */ jsx("div", { id: "network-view", class: "active", style: "display: block; height: 100%; margin-bottom: 2rem; overflow: hidden;", children: /* @__PURE__ */ jsxs("div", { style: "margin: 0 2rem; display: flex; gap: 1rem; height: 100%;", children: [
|
|
5534
6129
|
/* @__PURE__ */ jsx("div", { id: "requests-list-container", style: "flex: 1; height: 100%; border-radius: 6px; overflow: hidden; border: 1px solid var(--card-border);" }),
|
|
5535
|
-
/* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none; width: 500px; height: 100%; overflow
|
|
6130
|
+
/* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none; width: 500px; height: 100%; overflow: hidden; flex-shrink: 0; background: var(--bg-secondary); border: 1px solid var(--card-border); position: relative;", children: [
|
|
5536
6131
|
/* @__PURE__ */ jsx("div", { id: "details-drag-handle", style: "position: absolute; left: 0; top: 0; bottom: 0; width: 5px; cursor: col-resize; z-index: 11; background: transparent;" }),
|
|
5537
6132
|
/* @__PURE__ */ jsxs("div", { style: "display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; background: var(--bg-secondary); padding: 0.5rem 1rem; border-bottom: 1px solid var(--border-color); z-index: 10;", children: [
|
|
5538
|
-
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin: 0;", children: "Request Details" }),
|
|
6133
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin: 0; padding: 0", children: "Request Details" }),
|
|
5539
6134
|
/* @__PURE__ */ jsx("button", { onclick: "closeRequestDetails()", style: "background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.2rem;", children: "×" })
|
|
5540
6135
|
] }),
|
|
5541
|
-
/* @__PURE__ */
|
|
5542
|
-
/* @__PURE__ */ jsx("div", { id: "request-details-content" }),
|
|
5543
|
-
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
|
|
5544
|
-
/* @__PURE__ */ jsx("div", { id: "middleware-trace-container" })
|
|
5545
|
-
] })
|
|
6136
|
+
/* @__PURE__ */ jsx("div", { style: "display: flex; flex-direction: column; overflow: hidden; height: 100%", children: /* @__PURE__ */ jsx("div", { id: "request-details-content", style: "flex: 1; display: flex; flex-direction: column; height: 100%; overflow: hidden" }) })
|
|
5546
6137
|
] })
|
|
5547
6138
|
] }) })
|
|
5548
6139
|
] }),
|
|
@@ -5557,7 +6148,8 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
|
|
|
5557
6148
|
const getRequestHeaders = ${getRequestHeadersSource};
|
|
5558
6149
|
window.SHOKUPAN_CONFIG = {
|
|
5559
6150
|
rootPath: "${rootPath || ""}",
|
|
5560
|
-
linkPattern: "${linkPattern || ""}"
|
|
6151
|
+
linkPattern: "${linkPattern || ""}",
|
|
6152
|
+
ignorePaths: ${JSON.stringify(ignorePaths || [])}
|
|
5561
6153
|
};
|
|
5562
6154
|
`
|
|
5563
6155
|
} }),
|
|
@@ -5566,7 +6158,6 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
|
|
|
5566
6158
|
/* @__PURE__ */ jsx("script", { src: `${base}/charts.js` }),
|
|
5567
6159
|
/* @__PURE__ */ jsx("script", { src: `${base}/tables.js` }),
|
|
5568
6160
|
/* @__PURE__ */ jsx("script", { src: `${base}/registry.js` }),
|
|
5569
|
-
/* @__PURE__ */ jsx("script", { src: `${base}/failures.js` }),
|
|
5570
6161
|
/* @__PURE__ */ jsx("script", { src: `${base}/requests.js` }),
|
|
5571
6162
|
/* @__PURE__ */ jsx("script", { src: `${base}/tabs.js` })
|
|
5572
6163
|
] })
|
|
@@ -5635,15 +6226,51 @@ const require$1 = createRequire(import.meta.url);
|
|
|
5635
6226
|
const http = require$1("node:http");
|
|
5636
6227
|
const https = require$1("node:https");
|
|
5637
6228
|
class FetchInterceptor {
|
|
6229
|
+
static originalFetch;
|
|
6230
|
+
static originalHttpRequest;
|
|
6231
|
+
static originalHttpsRequest;
|
|
5638
6232
|
originalFetch;
|
|
5639
6233
|
originalHttpRequest;
|
|
5640
6234
|
originalHttpsRequest;
|
|
5641
6235
|
callbacks = [];
|
|
5642
6236
|
isPatched = false;
|
|
5643
6237
|
constructor() {
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
6238
|
+
if (!FetchInterceptor.originalFetch) {
|
|
6239
|
+
if (global.fetch.__isPatched) {
|
|
6240
|
+
console.warn("[FetchInterceptor] Global fetch is already patched! Cannot capture original.");
|
|
6241
|
+
} else {
|
|
6242
|
+
FetchInterceptor.originalFetch = global.fetch;
|
|
6243
|
+
FetchInterceptor.originalHttpRequest = http.request;
|
|
6244
|
+
FetchInterceptor.originalHttpsRequest = https.request;
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
this.originalFetch = FetchInterceptor.originalFetch || global.fetch;
|
|
6248
|
+
this.originalHttpRequest = FetchInterceptor.originalHttpRequest || http.request;
|
|
6249
|
+
this.originalHttpsRequest = FetchInterceptor.originalHttpsRequest || https.request;
|
|
6250
|
+
}
|
|
6251
|
+
/**
|
|
6252
|
+
* Statically restore the original network methods.
|
|
6253
|
+
* Useful for cleaning up in tests.
|
|
6254
|
+
*/
|
|
6255
|
+
/**
|
|
6256
|
+
* Statically restore the original network methods.
|
|
6257
|
+
* Useful for cleaning up in tests.
|
|
6258
|
+
*/
|
|
6259
|
+
static restore() {
|
|
6260
|
+
if (FetchInterceptor.originalFetch) {
|
|
6261
|
+
global.fetch = FetchInterceptor.originalFetch;
|
|
6262
|
+
} else if (global.fetch?.__originalFetch) {
|
|
6263
|
+
global.fetch = global.fetch.__originalFetch;
|
|
6264
|
+
} else if (typeof Bun !== "undefined" && Bun.fetch) {
|
|
6265
|
+
global.fetch = Bun.fetch;
|
|
6266
|
+
}
|
|
6267
|
+
if (FetchInterceptor.originalHttpRequest) {
|
|
6268
|
+
http.request = FetchInterceptor.originalHttpRequest;
|
|
6269
|
+
}
|
|
6270
|
+
if (FetchInterceptor.originalHttpsRequest) {
|
|
6271
|
+
https.request = FetchInterceptor.originalHttpsRequest;
|
|
6272
|
+
}
|
|
6273
|
+
console.log("[FetchInterceptor] Network layer restored (static).");
|
|
5647
6274
|
}
|
|
5648
6275
|
/**
|
|
5649
6276
|
* Patches the global `fetch` function to intercept requests.
|
|
@@ -5658,37 +6285,33 @@ class FetchInterceptor {
|
|
|
5658
6285
|
}
|
|
5659
6286
|
patchGlobalFetch() {
|
|
5660
6287
|
const self = this;
|
|
6288
|
+
if (!this.originalFetch && global.fetch.__isPatched && global.fetch.__originalFetch) {
|
|
6289
|
+
this.originalFetch = global.fetch.__originalFetch;
|
|
6290
|
+
}
|
|
5661
6291
|
const newFetch = async function(input, init) {
|
|
5662
6292
|
const startTime = performance.now();
|
|
5663
6293
|
const timestamp = Date.now();
|
|
5664
|
-
let method = "GET";
|
|
5665
6294
|
let url = "";
|
|
6295
|
+
let method = "GET";
|
|
5666
6296
|
let requestHeaders = {};
|
|
5667
|
-
let requestBody = void 0;
|
|
5668
6297
|
try {
|
|
5669
|
-
if (input
|
|
5670
|
-
url = input.toString();
|
|
5671
|
-
} else if (typeof input === "string") {
|
|
6298
|
+
if (typeof input === "string") {
|
|
5672
6299
|
url = input;
|
|
5673
|
-
} else if (
|
|
6300
|
+
} else if (input instanceof URL$1) {
|
|
6301
|
+
url = input.toString();
|
|
6302
|
+
} else if (input instanceof Request) {
|
|
5674
6303
|
url = input.url;
|
|
5675
6304
|
method = input.method;
|
|
6305
|
+
input.headers.forEach((v, k) => requestHeaders[k] = v);
|
|
5676
6306
|
}
|
|
5677
6307
|
if (init) {
|
|
5678
|
-
if (init.method) method = init.method;
|
|
6308
|
+
if (init.method) method = init.method.toUpperCase();
|
|
5679
6309
|
if (init.headers) {
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
} else if (Array.isArray(init.headers)) {
|
|
5683
|
-
init.headers.forEach(([k, v]) => requestHeaders[k] = v);
|
|
5684
|
-
} else {
|
|
5685
|
-
Object.assign(requestHeaders, init.headers);
|
|
5686
|
-
}
|
|
6310
|
+
const h = new Headers(init.headers);
|
|
6311
|
+
h.forEach((v, k) => requestHeaders[k] = v);
|
|
5687
6312
|
}
|
|
5688
|
-
if (init.body) requestBody = init.body;
|
|
5689
6313
|
}
|
|
5690
6314
|
} catch (e) {
|
|
5691
|
-
console.warn("[FetchInterceptor] Failed to parse request arguments", e);
|
|
5692
6315
|
}
|
|
5693
6316
|
try {
|
|
5694
6317
|
const response = await self.originalFetch.apply(global, [input, init]);
|
|
@@ -5698,14 +6321,11 @@ class FetchInterceptor {
|
|
|
5698
6321
|
method,
|
|
5699
6322
|
url,
|
|
5700
6323
|
requestHeaders,
|
|
5701
|
-
requestBody,
|
|
5702
|
-
status: response.status,
|
|
5703
6324
|
startTime: timestamp,
|
|
5704
6325
|
duration,
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
});
|
|
6326
|
+
status: response.status,
|
|
6327
|
+
...self.extractRequestMeta(url, requestHeaders)
|
|
6328
|
+
}).catch((err) => console.error("[FetchInterceptor] Error processing response:", err));
|
|
5709
6329
|
return response;
|
|
5710
6330
|
} catch (error) {
|
|
5711
6331
|
const duration = performance.now() - startTime;
|
|
@@ -5713,17 +6333,18 @@ class FetchInterceptor {
|
|
|
5713
6333
|
method,
|
|
5714
6334
|
url,
|
|
5715
6335
|
requestHeaders,
|
|
5716
|
-
requestBody,
|
|
5717
6336
|
status: 0,
|
|
5718
6337
|
responseHeaders: {},
|
|
5719
|
-
responseBody: `Network Error: ${String(error)}`,
|
|
5720
6338
|
startTime: timestamp,
|
|
5721
|
-
duration
|
|
6339
|
+
duration,
|
|
6340
|
+
responseBody: `Error: ${error.message}`,
|
|
6341
|
+
...self.extractRequestMeta(url, requestHeaders)
|
|
5722
6342
|
});
|
|
5723
6343
|
throw error;
|
|
5724
6344
|
}
|
|
5725
6345
|
};
|
|
5726
|
-
|
|
6346
|
+
newFetch.__isPatched = true;
|
|
6347
|
+
newFetch.__originalFetch = this.originalFetch;
|
|
5727
6348
|
global.fetch = newFetch;
|
|
5728
6349
|
}
|
|
5729
6350
|
patchNodeRequests() {
|
|
@@ -6068,6 +6689,9 @@ class Dashboard {
|
|
|
6068
6689
|
this.broadcastMetricUpdate(metric);
|
|
6069
6690
|
};
|
|
6070
6691
|
this.metricsCollector = new MetricsCollector(this.db, onCollect);
|
|
6692
|
+
if (app.applicationConfig) {
|
|
6693
|
+
app.applicationConfig.enableMiddlewareTracking = true;
|
|
6694
|
+
}
|
|
6071
6695
|
const fetchInterceptor = new FetchInterceptor();
|
|
6072
6696
|
fetchInterceptor.patch();
|
|
6073
6697
|
fetchInterceptor.on((log) => {
|
|
@@ -6101,6 +6725,10 @@ class Dashboard {
|
|
|
6101
6725
|
responseHeaders: log.responseHeaders
|
|
6102
6726
|
// No handler stack for outbound
|
|
6103
6727
|
};
|
|
6728
|
+
const maxLogs = this.dashboardConfig.maxLogEntries ?? 1e3;
|
|
6729
|
+
if (this.metrics.logs.length >= maxLogs) {
|
|
6730
|
+
this.metrics.logs.shift();
|
|
6731
|
+
}
|
|
6104
6732
|
this.metrics.logs.push(requestData);
|
|
6105
6733
|
const recordId = new RecordId("request", nanoid());
|
|
6106
6734
|
const idString = recordId.toString();
|
|
@@ -6128,17 +6756,9 @@ class Dashboard {
|
|
|
6128
6756
|
}
|
|
6129
6757
|
this.mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
6130
6758
|
const hooks = this.getHooks();
|
|
6131
|
-
if (
|
|
6132
|
-
app.
|
|
6759
|
+
if (hooks.onRequestStart) {
|
|
6760
|
+
app.hook("onRequestStart", hooks.onRequestStart);
|
|
6133
6761
|
}
|
|
6134
|
-
const hooksMiddleware = async (ctx, next) => {
|
|
6135
|
-
if (hooks.onRequestStart) {
|
|
6136
|
-
await hooks.onRequestStart(ctx);
|
|
6137
|
-
}
|
|
6138
|
-
ctx._startTime = performance.now();
|
|
6139
|
-
await next();
|
|
6140
|
-
};
|
|
6141
|
-
app.use(hooksMiddleware);
|
|
6142
6762
|
if (hooks.onResponseEnd) {
|
|
6143
6763
|
app.hook("onResponseEnd", hooks.onResponseEnd);
|
|
6144
6764
|
}
|
|
@@ -6385,7 +7005,7 @@ class Dashboard {
|
|
|
6385
7005
|
if (!this.instrumented && app) {
|
|
6386
7006
|
this.instrumentApp(app);
|
|
6387
7007
|
}
|
|
6388
|
-
const registry = app?.
|
|
7008
|
+
const registry = app?.registry;
|
|
6389
7009
|
if (registry) {
|
|
6390
7010
|
this.assignIdsToRegistry(registry, "root");
|
|
6391
7011
|
}
|
|
@@ -6422,23 +7042,48 @@ class Dashboard {
|
|
|
6422
7042
|
});
|
|
6423
7043
|
this.router.post("/replay", async (ctx) => {
|
|
6424
7044
|
const body = await ctx.body();
|
|
6425
|
-
const
|
|
6426
|
-
if (
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
headers
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
7045
|
+
const direction = body.direction || "inbound";
|
|
7046
|
+
if (direction === "outbound") {
|
|
7047
|
+
const start = performance.now();
|
|
7048
|
+
try {
|
|
7049
|
+
const res = await fetch(body.url, {
|
|
7050
|
+
method: body.method,
|
|
7051
|
+
headers: body.headers,
|
|
7052
|
+
body: body.body ? typeof body.body === "object" ? JSON.stringify(body.body) : body.body : void 0
|
|
7053
|
+
});
|
|
7054
|
+
const text = await res.text();
|
|
7055
|
+
const duration = performance.now() - start;
|
|
7056
|
+
const resHeaders = {};
|
|
7057
|
+
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
7058
|
+
return ctx.json({
|
|
7059
|
+
status: res.status,
|
|
7060
|
+
statusText: res.statusText,
|
|
7061
|
+
headers: resHeaders,
|
|
7062
|
+
data: text,
|
|
7063
|
+
duration
|
|
7064
|
+
});
|
|
7065
|
+
} catch (e) {
|
|
7066
|
+
return ctx.json({ error: String(e) }, 500);
|
|
7067
|
+
}
|
|
7068
|
+
} else {
|
|
7069
|
+
const app = this[$appRoot];
|
|
7070
|
+
if (!app) return unknownError(ctx);
|
|
7071
|
+
try {
|
|
7072
|
+
const result = await app.internalRequest({
|
|
7073
|
+
method: body.method,
|
|
7074
|
+
path: body.url,
|
|
7075
|
+
// or path
|
|
7076
|
+
headers: body.headers,
|
|
7077
|
+
body: body.body
|
|
7078
|
+
});
|
|
7079
|
+
return ctx.json({
|
|
7080
|
+
status: result.status,
|
|
7081
|
+
headers: result.headers,
|
|
7082
|
+
data: result.body
|
|
7083
|
+
});
|
|
7084
|
+
} catch (e) {
|
|
7085
|
+
return ctx.json({ error: String(e) }, 500);
|
|
7086
|
+
}
|
|
6442
7087
|
}
|
|
6443
7088
|
});
|
|
6444
7089
|
this.router.get("/**", async (ctx) => {
|
|
@@ -6476,6 +7121,14 @@ class Dashboard {
|
|
|
6476
7121
|
const linkPattern = this.getLinkPattern();
|
|
6477
7122
|
const integrations = this.detectIntegrations();
|
|
6478
7123
|
const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
|
|
7124
|
+
const ignorePaths = [
|
|
7125
|
+
...this.dashboardConfig.ignorePaths || [],
|
|
7126
|
+
// Add default ignores for integrations
|
|
7127
|
+
...Object.values(integrations).filter((p) => !!p).flatMap((p) => {
|
|
7128
|
+
const clean = p.endsWith("/") ? p.slice(0, -1) : p;
|
|
7129
|
+
return [clean, `${clean}/**`];
|
|
7130
|
+
})
|
|
7131
|
+
];
|
|
6479
7132
|
const html = renderToString(DashboardApp({
|
|
6480
7133
|
metrics: this.metrics,
|
|
6481
7134
|
uptime,
|
|
@@ -6483,7 +7136,8 @@ class Dashboard {
|
|
|
6483
7136
|
linkPattern,
|
|
6484
7137
|
integrations,
|
|
6485
7138
|
base: mountPath,
|
|
6486
|
-
getRequestHeadersSource
|
|
7139
|
+
getRequestHeadersSource,
|
|
7140
|
+
ignorePaths
|
|
6487
7141
|
}));
|
|
6488
7142
|
return ctx.html(`<!DOCTYPE html>${html}`);
|
|
6489
7143
|
});
|
|
@@ -6630,12 +7284,15 @@ class Dashboard {
|
|
|
6630
7284
|
getHooks() {
|
|
6631
7285
|
return {
|
|
6632
7286
|
onRequestStart: (ctx) => {
|
|
7287
|
+
if (ctx.path.startsWith(this.mountPath)) return;
|
|
6633
7288
|
const app = this[$appRoot];
|
|
6634
7289
|
if (!this.instrumented && app) {
|
|
6635
7290
|
this.instrumentApp(app);
|
|
6636
7291
|
}
|
|
6637
7292
|
this.metrics.totalRequests++;
|
|
6638
7293
|
this.metrics.activeRequests++;
|
|
7294
|
+
ctx._startTime = performance.now();
|
|
7295
|
+
ctx._reqStartTime = Date.now();
|
|
6639
7296
|
ctx[$debug] = new Collector(this);
|
|
6640
7297
|
if (!this.broadcastTimer) {
|
|
6641
7298
|
this.broadcastTimer = setTimeout(() => {
|
|
@@ -6725,7 +7382,7 @@ class Dashboard {
|
|
|
6725
7382
|
url: ctx.url.toString(),
|
|
6726
7383
|
status: response.status,
|
|
6727
7384
|
duration,
|
|
6728
|
-
timestamp: Date.now(),
|
|
7385
|
+
timestamp: ctx._reqStartTime || Date.now() - duration,
|
|
6729
7386
|
handlerStack: this.serializeHandlerStack(ctx.handlerStack),
|
|
6730
7387
|
body: this.serializeBody(ctx.responseBody),
|
|
6731
7388
|
requestBody: ctx.bodyData || ctx.requestBody,
|
|
@@ -6746,17 +7403,12 @@ class Dashboard {
|
|
|
6746
7403
|
responseHeaders: resHeaders
|
|
6747
7404
|
};
|
|
6748
7405
|
this.metrics.logs.push(logEntry);
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
...logEntry,
|
|
6754
|
-
direction: "inbound"
|
|
6755
|
-
}
|
|
6756
|
-
});
|
|
6757
|
-
} catch (e) {
|
|
7406
|
+
this.db.create(new RecordId("request", ctx.requestId), {
|
|
7407
|
+
...logEntry,
|
|
7408
|
+
direction: "inbound"
|
|
7409
|
+
}).catch((e) => {
|
|
6758
7410
|
console.error("Failed to record request log", e);
|
|
6759
|
-
}
|
|
7411
|
+
});
|
|
6760
7412
|
const retention = this.dashboardConfig.retentionMs ?? 72e5;
|
|
6761
7413
|
const cutoff = Date.now() - retention;
|
|
6762
7414
|
if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
|
|
@@ -7116,10 +7768,77 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
7116
7768
|
}
|
|
7117
7769
|
}
|
|
7118
7770
|
}
|
|
7771
|
+
function createLimitStream(maxSize) {
|
|
7772
|
+
let size = 0;
|
|
7773
|
+
return new TransformStream({
|
|
7774
|
+
transform(chunk, controller) {
|
|
7775
|
+
size += chunk.byteLength || chunk.length;
|
|
7776
|
+
if (size > maxSize) {
|
|
7777
|
+
controller.error(new Error(`Decompressed body size exceeded limit of ${maxSize} bytes`));
|
|
7778
|
+
} else {
|
|
7779
|
+
controller.enqueue(chunk);
|
|
7780
|
+
}
|
|
7781
|
+
}
|
|
7782
|
+
});
|
|
7783
|
+
}
|
|
7119
7784
|
function Compression(options = {}) {
|
|
7120
7785
|
const threshold = options.threshold ?? 512;
|
|
7121
7786
|
const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
|
|
7787
|
+
const decompress = options.decompress ?? true;
|
|
7788
|
+
const maxDecompressedSize = options.maxDecompressedSize ?? 10 * 1024 * 1024;
|
|
7122
7789
|
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
7790
|
+
const requestEncoding = ctx.headers.get("content-encoding");
|
|
7791
|
+
if (decompress && requestEncoding && !ctx.headers.get("content-encoding")?.includes("identity") && ctx.req.body) {
|
|
7792
|
+
let stream = null;
|
|
7793
|
+
if (requestEncoding.includes("br")) {
|
|
7794
|
+
const decompressor = zlib.createBrotliDecompress();
|
|
7795
|
+
const nodeStream = Readable.fromWeb(ctx.req.body);
|
|
7796
|
+
stream = Readable.toWeb(nodeStream.pipe(decompressor));
|
|
7797
|
+
} else if (requestEncoding.includes("gzip")) {
|
|
7798
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
7799
|
+
stream = ctx.req.body.pipeThrough(new DecompressionStream("gzip"));
|
|
7800
|
+
} else {
|
|
7801
|
+
const decompressor = zlib.createGunzip();
|
|
7802
|
+
const nodeStream = Readable.fromWeb(ctx.req.body);
|
|
7803
|
+
stream = Readable.toWeb(nodeStream.pipe(decompressor));
|
|
7804
|
+
}
|
|
7805
|
+
} else if (requestEncoding.includes("deflate")) {
|
|
7806
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
7807
|
+
stream = ctx.req.body.pipeThrough(new DecompressionStream("deflate"));
|
|
7808
|
+
} else {
|
|
7809
|
+
const decompressor = zlib.createInflate();
|
|
7810
|
+
const nodeStream = Readable.fromWeb(ctx.req.body);
|
|
7811
|
+
stream = Readable.toWeb(nodeStream.pipe(decompressor));
|
|
7812
|
+
}
|
|
7813
|
+
}
|
|
7814
|
+
if (stream) {
|
|
7815
|
+
const outputStream = stream.pipeThrough(createLimitStream(maxDecompressedSize));
|
|
7816
|
+
const originalIp = ctx.ip;
|
|
7817
|
+
const originalReq = ctx.req;
|
|
7818
|
+
const newHeaders = new Headers(originalReq.headers);
|
|
7819
|
+
newHeaders.delete("content-encoding");
|
|
7820
|
+
newHeaders.delete("content-length");
|
|
7821
|
+
const newReq = new Proxy(originalReq, {
|
|
7822
|
+
get(target, prop, receiver) {
|
|
7823
|
+
if (prop === "body") return outputStream;
|
|
7824
|
+
if (prop === "headers") return newHeaders;
|
|
7825
|
+
if (prop === "json") return async () => JSON.parse(await new Response(outputStream).text());
|
|
7826
|
+
if (prop === "text") return async () => await new Response(outputStream).text();
|
|
7827
|
+
if (prop === "arrayBuffer") return async () => await new Response(outputStream).arrayBuffer();
|
|
7828
|
+
if (prop === "blob") return async () => await new Response(outputStream).blob();
|
|
7829
|
+
if (prop === "formData") return async () => await new Response(outputStream).formData();
|
|
7830
|
+
return Reflect.get(target, prop, target);
|
|
7831
|
+
}
|
|
7832
|
+
});
|
|
7833
|
+
ctx.request = newReq;
|
|
7834
|
+
if (originalIp) {
|
|
7835
|
+
Object.defineProperty(ctx, "ip", {
|
|
7836
|
+
configurable: true,
|
|
7837
|
+
get: () => originalIp
|
|
7838
|
+
});
|
|
7839
|
+
}
|
|
7840
|
+
}
|
|
7841
|
+
}
|
|
7123
7842
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
7124
7843
|
let method = null;
|
|
7125
7844
|
if (acceptEncoding.includes("br")) method = "br";
|
|
@@ -7571,7 +8290,7 @@ function openApiValidator() {
|
|
|
7571
8290
|
if (validators.body) {
|
|
7572
8291
|
let body;
|
|
7573
8292
|
try {
|
|
7574
|
-
body = await ctx.
|
|
8293
|
+
body = await ctx.body();
|
|
7575
8294
|
} catch {
|
|
7576
8295
|
body = {};
|
|
7577
8296
|
}
|
|
@@ -7693,8 +8412,7 @@ function enableOpenApiValidation(app) {
|
|
|
7693
8412
|
}
|
|
7694
8413
|
function SecurityHeaders(options = {}) {
|
|
7695
8414
|
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
7696
|
-
const
|
|
7697
|
-
const set = (k, v) => headers[k] = v;
|
|
8415
|
+
const set = (k, v) => ctx.response.set(k, v);
|
|
7698
8416
|
if (options.dnsPrefetchControl !== false) {
|
|
7699
8417
|
const allow = options.dnsPrefetchControl?.allow;
|
|
7700
8418
|
set("X-DNS-Prefetch-Control", allow ? "on" : "off");
|
|
@@ -7740,14 +8458,6 @@ function SecurityHeaders(options = {}) {
|
|
|
7740
8458
|
}
|
|
7741
8459
|
if (options.hidePoweredBy !== false) ;
|
|
7742
8460
|
const response = await next();
|
|
7743
|
-
if (response instanceof Response) {
|
|
7744
|
-
const headerEntries = Object.entries(headers);
|
|
7745
|
-
for (let i = 0; i < headerEntries.length; i++) {
|
|
7746
|
-
const [k, v] = headerEntries[i];
|
|
7747
|
-
response.headers.set(k, v);
|
|
7748
|
-
}
|
|
7749
|
-
return response;
|
|
7750
|
-
}
|
|
7751
8461
|
return response;
|
|
7752
8462
|
};
|
|
7753
8463
|
securityHeadersMiddleware.isBuiltin = true;
|
|
@@ -8024,6 +8734,7 @@ export {
|
|
|
8024
8734
|
$bodyParsed,
|
|
8025
8735
|
$bodyType,
|
|
8026
8736
|
$cachedBody,
|
|
8737
|
+
$cachedCookies,
|
|
8027
8738
|
$cachedHost,
|
|
8028
8739
|
$cachedHostname,
|
|
8029
8740
|
$cachedOrigin,
|