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.cjs
CHANGED
|
@@ -44,7 +44,8 @@ const os = require("node:os");
|
|
|
44
44
|
const node_module = require("node:module");
|
|
45
45
|
const node_perf_hooks = require("node:perf_hooks");
|
|
46
46
|
const fs$1 = require("node:fs");
|
|
47
|
-
const analyzer = require("./analyzer-
|
|
47
|
+
const analyzer = require("./analyzer-DM-OlRq8.cjs");
|
|
48
|
+
const node_stream = require("node:stream");
|
|
48
49
|
const zlib = require("node:zlib");
|
|
49
50
|
const Ajv = require("ajv");
|
|
50
51
|
const addFormats = require("ajv-formats");
|
|
@@ -70,6 +71,114 @@ function _interopNamespaceDefault(e) {
|
|
|
70
71
|
const http__namespace = /* @__PURE__ */ _interopNamespaceDefault(http$1);
|
|
71
72
|
const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
|
|
72
73
|
const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
|
|
74
|
+
class BodyParser {
|
|
75
|
+
/**
|
|
76
|
+
* Parses the body of a request based on Content-Type header.
|
|
77
|
+
* @param req The ShokupanRequest object
|
|
78
|
+
* @param config Application configuration for limits and parser options
|
|
79
|
+
* @returns The parsed body or throws an error
|
|
80
|
+
*/
|
|
81
|
+
static async parse(req, config = {}) {
|
|
82
|
+
const contentType = req.headers.get("content-type") || "";
|
|
83
|
+
const maxBodySize = config.maxBodySize ?? 10 * 1024 * 1024;
|
|
84
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
85
|
+
return {
|
|
86
|
+
type: "json",
|
|
87
|
+
body: await BodyParser.parseJson(req, config.jsonParser || "native", maxBodySize)
|
|
88
|
+
};
|
|
89
|
+
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
90
|
+
return {
|
|
91
|
+
type: "formData",
|
|
92
|
+
body: await BodyParser.parseFormData(req, maxBodySize)
|
|
93
|
+
};
|
|
94
|
+
} else {
|
|
95
|
+
return {
|
|
96
|
+
type: "text",
|
|
97
|
+
body: await BodyParser.readRawBody(req, maxBodySize)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parsing helper for JSON
|
|
103
|
+
*/
|
|
104
|
+
static async parseJson(req, parserType, maxBodySize) {
|
|
105
|
+
const rawText = await BodyParser.readRawBody(req, maxBodySize);
|
|
106
|
+
if (parserType === "native") {
|
|
107
|
+
if (!rawText) return {};
|
|
108
|
+
return JSON.parse(rawText);
|
|
109
|
+
} else {
|
|
110
|
+
const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
|
|
111
|
+
const parser = getJSONParser(parserType);
|
|
112
|
+
return parser(rawText);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parsing helper for FormData
|
|
117
|
+
*/
|
|
118
|
+
static async parseFormData(req, maxBodySize) {
|
|
119
|
+
const clHeader = req.headers.get("content-length");
|
|
120
|
+
if (!clHeader) {
|
|
121
|
+
const err = new Error("Length Required");
|
|
122
|
+
err.status = 411;
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
const cl = parseInt(clHeader, 10);
|
|
126
|
+
if (isNaN(cl)) {
|
|
127
|
+
const err = new Error("Bad Request");
|
|
128
|
+
err.status = 400;
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
if (cl > maxBodySize) {
|
|
132
|
+
const err = new Error("Payload Too Large");
|
|
133
|
+
err.status = 413;
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
return req.formData();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Reads raw body as string with size enforcement
|
|
140
|
+
*/
|
|
141
|
+
static async readRawBody(req, maxBodySize) {
|
|
142
|
+
if (typeof req.body === "string") {
|
|
143
|
+
const body = req.body;
|
|
144
|
+
if (body.length > maxBodySize) {
|
|
145
|
+
const err = new Error("Payload Too Large");
|
|
146
|
+
err.status = 413;
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
return body;
|
|
150
|
+
}
|
|
151
|
+
const reader = req.body?.getReader();
|
|
152
|
+
if (!reader) {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
const chunks = [];
|
|
156
|
+
let totalSize = 0;
|
|
157
|
+
try {
|
|
158
|
+
while (true) {
|
|
159
|
+
const { done, value } = await reader.read();
|
|
160
|
+
if (done) break;
|
|
161
|
+
totalSize += value.length;
|
|
162
|
+
if (totalSize > maxBodySize) {
|
|
163
|
+
const err = new Error("Payload Too Large");
|
|
164
|
+
err.status = 413;
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
chunks.push(value);
|
|
168
|
+
}
|
|
169
|
+
} finally {
|
|
170
|
+
reader.releaseLock();
|
|
171
|
+
}
|
|
172
|
+
const result = new Uint8Array(totalSize);
|
|
173
|
+
let offset = 0;
|
|
174
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
175
|
+
const chunk = chunks[i];
|
|
176
|
+
result.set(chunk, offset);
|
|
177
|
+
offset += chunk.length;
|
|
178
|
+
}
|
|
179
|
+
return new TextDecoder().decode(result);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
73
182
|
const HTTP_STATUS = {
|
|
74
183
|
// 2xx Success
|
|
75
184
|
OK: 200,
|
|
@@ -260,6 +369,7 @@ const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol"
|
|
|
260
369
|
const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
|
|
261
370
|
const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
|
|
262
371
|
const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
|
|
372
|
+
const $cachedCookies = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedCookies");
|
|
263
373
|
const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
|
|
264
374
|
const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
|
|
265
375
|
const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
|
|
@@ -318,6 +428,7 @@ class ShokupanContext {
|
|
|
318
428
|
[$cachedHost];
|
|
319
429
|
[$cachedOrigin];
|
|
320
430
|
[$cachedQuery];
|
|
431
|
+
[$cachedCookies];
|
|
321
432
|
disconnectCallbacks = [];
|
|
322
433
|
/**
|
|
323
434
|
* Registers a callback to be executed when the associated WebSocket disconnects.
|
|
@@ -415,13 +526,20 @@ class ShokupanContext {
|
|
|
415
526
|
if (this[$cachedQuery]) return this[$cachedQuery];
|
|
416
527
|
const q = /* @__PURE__ */ Object.create(null);
|
|
417
528
|
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
529
|
+
const mode = this.app?.applicationConfig?.queryParserMode || "extended";
|
|
418
530
|
this.url.searchParams.forEach((value, key) => {
|
|
419
531
|
if (blocklist.includes(key)) return;
|
|
420
532
|
if (Object.prototype.hasOwnProperty.call(q, key)) {
|
|
421
|
-
if (
|
|
422
|
-
|
|
533
|
+
if (mode === "strict") {
|
|
534
|
+
throw new Error(`Duplicate query parameter '${key}' is not allowed in strict mode.`);
|
|
535
|
+
} else if (mode === "simple") {
|
|
536
|
+
q[key] = value;
|
|
423
537
|
} else {
|
|
424
|
-
|
|
538
|
+
if (Array.isArray(q[key])) {
|
|
539
|
+
q[key].push(value);
|
|
540
|
+
} else {
|
|
541
|
+
q[key] = [q[key], value];
|
|
542
|
+
}
|
|
425
543
|
}
|
|
426
544
|
} else {
|
|
427
545
|
q[key] = value;
|
|
@@ -430,6 +548,28 @@ class ShokupanContext {
|
|
|
430
548
|
this[$cachedQuery] = q;
|
|
431
549
|
return q;
|
|
432
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Request cookies
|
|
553
|
+
*/
|
|
554
|
+
get cookies() {
|
|
555
|
+
if (this[$cachedCookies]) return this[$cachedCookies];
|
|
556
|
+
const c = /* @__PURE__ */ Object.create(null);
|
|
557
|
+
const cookieHeader = this.request.headers.get("cookie");
|
|
558
|
+
if (cookieHeader) {
|
|
559
|
+
const pairs = cookieHeader.split(";");
|
|
560
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
561
|
+
const pair = pairs[i];
|
|
562
|
+
const index = pair.indexOf("=");
|
|
563
|
+
if (index > 0) {
|
|
564
|
+
const key = pair.slice(0, index).trim();
|
|
565
|
+
const value = pair.slice(index + 1).trim();
|
|
566
|
+
c[key] = decodeURIComponent(value);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
this[$cachedCookies] = c;
|
|
571
|
+
return c;
|
|
572
|
+
}
|
|
433
573
|
/**
|
|
434
574
|
* Client IP address
|
|
435
575
|
*/
|
|
@@ -604,6 +744,10 @@ class ShokupanContext {
|
|
|
604
744
|
}
|
|
605
745
|
return h;
|
|
606
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* Read request body with caching to avoid double parsing.
|
|
749
|
+
* The body is only parsed once and cached for subsequent reads.
|
|
750
|
+
*/
|
|
607
751
|
/**
|
|
608
752
|
* Read request body with caching to avoid double parsing.
|
|
609
753
|
* The body is only parsed once and cached for subsequent reads.
|
|
@@ -615,29 +759,10 @@ class ShokupanContext {
|
|
|
615
759
|
if (this[$bodyParsed] === true) {
|
|
616
760
|
return this[$cachedBody];
|
|
617
761
|
}
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
try {
|
|
623
|
-
this[$cachedBody] = await this.request.json();
|
|
624
|
-
} catch (e) {
|
|
625
|
-
throw e;
|
|
626
|
-
}
|
|
627
|
-
} else {
|
|
628
|
-
const rawText = await this.request.text();
|
|
629
|
-
const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
|
|
630
|
-
const parser = getJSONParser(parserType);
|
|
631
|
-
this[$cachedBody] = parser(rawText);
|
|
632
|
-
}
|
|
633
|
-
this[$bodyType] = "json";
|
|
634
|
-
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
635
|
-
this[$cachedBody] = await this.request.formData();
|
|
636
|
-
this[$bodyType] = "formData";
|
|
637
|
-
} else {
|
|
638
|
-
this[$cachedBody] = await this.request.text();
|
|
639
|
-
this[$bodyType] = "text";
|
|
640
|
-
}
|
|
762
|
+
const config = this.app?.applicationConfig || {};
|
|
763
|
+
const { type, body } = await BodyParser.parse(this.request, config);
|
|
764
|
+
this[$bodyType] = type;
|
|
765
|
+
this[$cachedBody] = body;
|
|
641
766
|
this[$bodyParsed] = true;
|
|
642
767
|
return this[$cachedBody];
|
|
643
768
|
}
|
|
@@ -653,45 +778,22 @@ class ShokupanContext {
|
|
|
653
778
|
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
654
779
|
return;
|
|
655
780
|
}
|
|
781
|
+
const maxBodySize = this.app?.applicationConfig?.maxBodySize ?? 10 * 1024 * 1024;
|
|
782
|
+
const contentLength = parseInt(this.request.headers.get("content-length") || "0", 10);
|
|
783
|
+
if (contentLength > maxBodySize) {
|
|
784
|
+
this[$bodyParseError] = new Error("Payload Too Large");
|
|
785
|
+
this[$bodyParseError].status = 413;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
656
788
|
try {
|
|
657
789
|
await this.body();
|
|
658
790
|
} catch (error) {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
* Read raw body from ReadableStream efficiently.
|
|
664
|
-
* This is much faster than request.text() for large payloads.
|
|
665
|
-
* Also handles the case where body is already a string (e.g., in tests).
|
|
666
|
-
*/
|
|
667
|
-
async readRawBody() {
|
|
668
|
-
if (typeof this.request.body === "string") {
|
|
669
|
-
return this.request.body;
|
|
670
|
-
}
|
|
671
|
-
const reader = this.request.body?.getReader();
|
|
672
|
-
if (!reader) {
|
|
673
|
-
return "";
|
|
674
|
-
}
|
|
675
|
-
const chunks = [];
|
|
676
|
-
let totalSize = 0;
|
|
677
|
-
try {
|
|
678
|
-
while (true) {
|
|
679
|
-
const { done, value } = await reader.read();
|
|
680
|
-
if (done) break;
|
|
681
|
-
chunks.push(value);
|
|
682
|
-
totalSize += value.length;
|
|
791
|
+
if (error.status === 413 || error.message === "Payload Too Large") {
|
|
792
|
+
this[$bodyParseError] = error;
|
|
793
|
+
} else {
|
|
794
|
+
this[$bodyParseError] = error;
|
|
683
795
|
}
|
|
684
|
-
} finally {
|
|
685
|
-
reader.releaseLock();
|
|
686
796
|
}
|
|
687
|
-
const result = new Uint8Array(totalSize);
|
|
688
|
-
let offset = 0;
|
|
689
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
690
|
-
const chunk = chunks[i];
|
|
691
|
-
result.set(chunk, offset);
|
|
692
|
-
offset += chunk.length;
|
|
693
|
-
}
|
|
694
|
-
return new TextDecoder().decode(result);
|
|
695
797
|
}
|
|
696
798
|
/**
|
|
697
799
|
* Send a response
|
|
@@ -791,7 +893,15 @@ class ShokupanContext {
|
|
|
791
893
|
}
|
|
792
894
|
this.response.status = status;
|
|
793
895
|
const finalHeaders = this.mergeHeaders();
|
|
794
|
-
|
|
896
|
+
const targetUrl = url instanceof Promise ? await url : url;
|
|
897
|
+
if (targetUrl.startsWith("//")) {
|
|
898
|
+
throw new Error("Invalid redirect: Protocol-relative URLs are not allowed.");
|
|
899
|
+
}
|
|
900
|
+
const lowerUrl = targetUrl.toLowerCase();
|
|
901
|
+
if (lowerUrl.startsWith("javascript:") || lowerUrl.startsWith("data:") || lowerUrl.startsWith("vbscript:")) {
|
|
902
|
+
throw new Error(`Invalid redirect: Unsafe protocol '${targetUrl.split(":")[0]}'`);
|
|
903
|
+
}
|
|
904
|
+
finalHeaders.set("Location", targetUrl);
|
|
795
905
|
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
796
906
|
return this[$finalResponse];
|
|
797
907
|
}
|
|
@@ -849,6 +959,185 @@ class ShokupanContext {
|
|
|
849
959
|
const html = await this.renderer(element, args);
|
|
850
960
|
return this.html(html, status, headers);
|
|
851
961
|
}
|
|
962
|
+
/**
|
|
963
|
+
* Pipe a ReadableStream to the response
|
|
964
|
+
* @param stream ReadableStream to pipe
|
|
965
|
+
* @param options Response options (status, headers)
|
|
966
|
+
*/
|
|
967
|
+
pipe(stream, options) {
|
|
968
|
+
const headers = this.mergeHeaders(options?.headers);
|
|
969
|
+
const status = options?.status ?? this.response.status ?? 200;
|
|
970
|
+
if (this.app?.applicationConfig?.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
971
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
972
|
+
}
|
|
973
|
+
this[$finalResponse] = new Response(stream, { status, headers });
|
|
974
|
+
return this[$finalResponse];
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Internal helper to create a streaming response with common infrastructure
|
|
978
|
+
* @private
|
|
979
|
+
*/
|
|
980
|
+
createStreamHelper(helperFactory, callback, onError, headers) {
|
|
981
|
+
let controller;
|
|
982
|
+
const aborted = { value: false };
|
|
983
|
+
const abortCallbacks = [];
|
|
984
|
+
const encoder = new TextEncoder();
|
|
985
|
+
let helper;
|
|
986
|
+
const stream = new ReadableStream({
|
|
987
|
+
start(ctrl) {
|
|
988
|
+
controller = ctrl;
|
|
989
|
+
helper = helperFactory(controller, aborted, abortCallbacks, encoder);
|
|
990
|
+
(async () => {
|
|
991
|
+
try {
|
|
992
|
+
await callback(helper);
|
|
993
|
+
controller.close();
|
|
994
|
+
} catch (err) {
|
|
995
|
+
if (onError) {
|
|
996
|
+
try {
|
|
997
|
+
await onError(err, helper);
|
|
998
|
+
} catch (handlerErr) {
|
|
999
|
+
console.error("Error in stream error handler:", handlerErr);
|
|
1000
|
+
}
|
|
1001
|
+
} else {
|
|
1002
|
+
console.error("Stream error:", err);
|
|
1003
|
+
}
|
|
1004
|
+
if (!aborted.value) {
|
|
1005
|
+
controller.close();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
})();
|
|
1009
|
+
},
|
|
1010
|
+
async pull() {
|
|
1011
|
+
},
|
|
1012
|
+
cancel() {
|
|
1013
|
+
aborted.value = true;
|
|
1014
|
+
abortCallbacks.forEach((cb) => {
|
|
1015
|
+
try {
|
|
1016
|
+
cb();
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
console.error("Error in abort callback:", err);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
return this.pipe(stream, { headers });
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Generic streaming helper for binary/text data
|
|
1027
|
+
* @param callback Callback function that receives a StreamHelper
|
|
1028
|
+
* @param onError Optional error handler
|
|
1029
|
+
*/
|
|
1030
|
+
stream(callback, onError) {
|
|
1031
|
+
return this.createStreamHelper(
|
|
1032
|
+
(controller, aborted, abortCallbacks, encoder) => ({
|
|
1033
|
+
async write(data) {
|
|
1034
|
+
if (aborted.value) return;
|
|
1035
|
+
const chunk = typeof data === "string" ? encoder.encode(data) : data;
|
|
1036
|
+
controller.enqueue(chunk);
|
|
1037
|
+
},
|
|
1038
|
+
async pipe(stream) {
|
|
1039
|
+
if (aborted.value) return;
|
|
1040
|
+
const reader = stream.getReader();
|
|
1041
|
+
try {
|
|
1042
|
+
while (true) {
|
|
1043
|
+
const { done, value } = await reader.read();
|
|
1044
|
+
if (done || aborted.value) break;
|
|
1045
|
+
controller.enqueue(value);
|
|
1046
|
+
}
|
|
1047
|
+
} finally {
|
|
1048
|
+
reader.releaseLock();
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
sleep(ms) {
|
|
1052
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1053
|
+
},
|
|
1054
|
+
onAbort(callback2) {
|
|
1055
|
+
abortCallbacks.push(callback2);
|
|
1056
|
+
}
|
|
1057
|
+
}),
|
|
1058
|
+
callback,
|
|
1059
|
+
onError
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Text streaming helper with proper headers
|
|
1064
|
+
* @param callback Callback function that receives a TextStreamHelper
|
|
1065
|
+
* @param onError Optional error handler
|
|
1066
|
+
*/
|
|
1067
|
+
streamText(callback, onError) {
|
|
1068
|
+
const headers = new Headers(this.response.headers);
|
|
1069
|
+
headers.set("Content-Type", "text/plain; charset=utf-8");
|
|
1070
|
+
headers.set("Transfer-Encoding", "chunked");
|
|
1071
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
1072
|
+
return this.createStreamHelper(
|
|
1073
|
+
(controller, aborted, abortCallbacks, encoder) => ({
|
|
1074
|
+
async write(text) {
|
|
1075
|
+
if (aborted.value) return;
|
|
1076
|
+
controller.enqueue(encoder.encode(text));
|
|
1077
|
+
},
|
|
1078
|
+
async writeln(text) {
|
|
1079
|
+
if (aborted.value) return;
|
|
1080
|
+
controller.enqueue(encoder.encode(text + "\n"));
|
|
1081
|
+
},
|
|
1082
|
+
sleep(ms) {
|
|
1083
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1084
|
+
},
|
|
1085
|
+
onAbort(callback2) {
|
|
1086
|
+
abortCallbacks.push(callback2);
|
|
1087
|
+
}
|
|
1088
|
+
}),
|
|
1089
|
+
callback,
|
|
1090
|
+
onError,
|
|
1091
|
+
headers
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Server-Sent Events (SSE) streaming helper
|
|
1096
|
+
* @param callback Callback function that receives an SSEStreamHelper
|
|
1097
|
+
* @param onError Optional error handler
|
|
1098
|
+
*/
|
|
1099
|
+
streamSSE(callback, onError) {
|
|
1100
|
+
const headers = new Headers(this.response.headers);
|
|
1101
|
+
headers.set("Content-Type", "text/event-stream");
|
|
1102
|
+
headers.set("Cache-Control", "no-cache");
|
|
1103
|
+
headers.set("Connection", "keep-alive");
|
|
1104
|
+
return this.createStreamHelper(
|
|
1105
|
+
(controller, aborted, abortCallbacks, encoder) => ({
|
|
1106
|
+
async writeSSE(message) {
|
|
1107
|
+
if (aborted.value) return;
|
|
1108
|
+
let sseMessage = "";
|
|
1109
|
+
if (message.event) {
|
|
1110
|
+
sseMessage += `event: ${message.event}
|
|
1111
|
+
`;
|
|
1112
|
+
}
|
|
1113
|
+
if (message.id !== void 0) {
|
|
1114
|
+
sseMessage += `id: ${message.id}
|
|
1115
|
+
`;
|
|
1116
|
+
}
|
|
1117
|
+
if (message.retry !== void 0) {
|
|
1118
|
+
sseMessage += `retry: ${message.retry}
|
|
1119
|
+
`;
|
|
1120
|
+
}
|
|
1121
|
+
const dataLines = message.data.split("\n");
|
|
1122
|
+
for (const line of dataLines) {
|
|
1123
|
+
sseMessage += `data: ${line}
|
|
1124
|
+
`;
|
|
1125
|
+
}
|
|
1126
|
+
sseMessage += "\n";
|
|
1127
|
+
controller.enqueue(encoder.encode(sseMessage));
|
|
1128
|
+
},
|
|
1129
|
+
sleep(ms) {
|
|
1130
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1131
|
+
},
|
|
1132
|
+
onAbort(callback2) {
|
|
1133
|
+
abortCallbacks.push(callback2);
|
|
1134
|
+
}
|
|
1135
|
+
}),
|
|
1136
|
+
callback,
|
|
1137
|
+
onError,
|
|
1138
|
+
headers
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
852
1141
|
}
|
|
853
1142
|
const compose = (middleware) => {
|
|
854
1143
|
if (!middleware.length) {
|
|
@@ -865,6 +1154,11 @@ const compose = (middleware) => {
|
|
|
865
1154
|
return next ? next() : Promise.resolve();
|
|
866
1155
|
}
|
|
867
1156
|
const fn = middleware[i];
|
|
1157
|
+
if (typeof fn !== "function") {
|
|
1158
|
+
const name = fn?.constructor?.name;
|
|
1159
|
+
console.error(`[Middleware Error] Item at index ${i} is not a function! It is: ${typeof fn} (${name})`, fn);
|
|
1160
|
+
throw new TypeError(`Middleware at index ${i} must be a function, got ${name}`);
|
|
1161
|
+
}
|
|
868
1162
|
if (!context[$debug]) {
|
|
869
1163
|
return fn(context, () => runner(i + 1));
|
|
870
1164
|
}
|
|
@@ -1154,7 +1448,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1154
1448
|
let astMiddlewareRegistry = {};
|
|
1155
1449
|
let applications = [];
|
|
1156
1450
|
try {
|
|
1157
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-
|
|
1451
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-DM-OlRq8.cjs"));
|
|
1158
1452
|
const entrypoint = rootRouter.metadata?.file;
|
|
1159
1453
|
const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
1160
1454
|
const analysisResult = await analyzer2.analyze();
|
|
@@ -1697,8 +1991,11 @@ function serveStatic(config, prefix) {
|
|
|
1697
1991
|
if (typeof Bun !== "undefined") {
|
|
1698
1992
|
response = new Response(Bun.file(finalPath));
|
|
1699
1993
|
} else {
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1994
|
+
const { createReadStream } = await import("node:fs");
|
|
1995
|
+
const { Readable } = await import("node:stream");
|
|
1996
|
+
const fileStream = createReadStream(finalPath);
|
|
1997
|
+
const webStream = Readable.toWeb(fileStream);
|
|
1998
|
+
response = new Response(webStream);
|
|
1702
1999
|
}
|
|
1703
2000
|
if (config.hooks?.onResponse) {
|
|
1704
2001
|
const hooked = await config.hooks.onResponse(ctx, response);
|
|
@@ -1710,6 +2007,37 @@ function serveStatic(config, prefix) {
|
|
|
1710
2007
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1711
2008
|
return serveStaticMiddleware;
|
|
1712
2009
|
}
|
|
2010
|
+
const metadataStore = /* @__PURE__ */ new WeakMap();
|
|
2011
|
+
function defineMetadata(key, value, target, propertyKey) {
|
|
2012
|
+
let targetMetadata = metadataStore.get(target);
|
|
2013
|
+
if (!targetMetadata) {
|
|
2014
|
+
targetMetadata = /* @__PURE__ */ new Map();
|
|
2015
|
+
metadataStore.set(target, targetMetadata);
|
|
2016
|
+
}
|
|
2017
|
+
const storageKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : key;
|
|
2018
|
+
targetMetadata.set(storageKey, value);
|
|
2019
|
+
}
|
|
2020
|
+
function getMetadata(key, target, propertyKey) {
|
|
2021
|
+
const targetMetadata = metadataStore.get(target);
|
|
2022
|
+
if (!targetMetadata) return void 0;
|
|
2023
|
+
const storageKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : key;
|
|
2024
|
+
return targetMetadata.get(storageKey);
|
|
2025
|
+
}
|
|
2026
|
+
if (typeof Reflect === "object") {
|
|
2027
|
+
if (!Reflect.defineMetadata) {
|
|
2028
|
+
Reflect.defineMetadata = defineMetadata;
|
|
2029
|
+
}
|
|
2030
|
+
if (!Reflect.getMetadata) {
|
|
2031
|
+
Reflect.getMetadata = getMetadata;
|
|
2032
|
+
}
|
|
2033
|
+
if (!Reflect.metadata) {
|
|
2034
|
+
Reflect.metadata = function(metadataKey, metadataValue) {
|
|
2035
|
+
return function decorator(target, propertyKey) {
|
|
2036
|
+
defineMetadata(metadataKey, metadataValue, target, propertyKey);
|
|
2037
|
+
};
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
1713
2041
|
class Container {
|
|
1714
2042
|
static services = /* @__PURE__ */ new Map();
|
|
1715
2043
|
static register(target, instance) {
|
|
@@ -1721,28 +2049,60 @@ class Container {
|
|
|
1721
2049
|
static has(target) {
|
|
1722
2050
|
return this.services.has(target);
|
|
1723
2051
|
}
|
|
2052
|
+
static cache = /* @__PURE__ */ new Map();
|
|
2053
|
+
static resolvingStack = /* @__PURE__ */ new Set();
|
|
1724
2054
|
static resolve(target) {
|
|
1725
2055
|
if (this.services.has(target)) {
|
|
1726
2056
|
return this.services.get(target);
|
|
1727
2057
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
2058
|
+
if (this.resolvingStack.has(target)) {
|
|
2059
|
+
const cycle = Array.from(this.resolvingStack);
|
|
2060
|
+
cycle.push(target);
|
|
2061
|
+
throw new Error(`Circular dependency detected: ${cycle.map((t) => t.name || t).join(" -> ")}`);
|
|
2062
|
+
}
|
|
2063
|
+
this.resolvingStack.add(target);
|
|
2064
|
+
try {
|
|
2065
|
+
let meta = this.cache.get(target);
|
|
2066
|
+
if (!meta) {
|
|
2067
|
+
const scope = Reflect.getMetadata("di:scope", target) || "singleton";
|
|
2068
|
+
const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
|
|
2069
|
+
const manualTokens = Reflect.getMetadata("di:constructor:params", target) || [];
|
|
2070
|
+
const dependencies = paramTypes.map((param, index) => {
|
|
2071
|
+
const manual = manualTokens.find((t) => t.index === index);
|
|
2072
|
+
if (manual && manual.token) return manual.token;
|
|
2073
|
+
if (param === String || param === Number || param === Boolean || param === Object || param === void 0) return void 0;
|
|
2074
|
+
return param;
|
|
2075
|
+
});
|
|
2076
|
+
meta = { scope, dependencies };
|
|
2077
|
+
this.cache.set(target, meta);
|
|
2078
|
+
}
|
|
2079
|
+
const args = meta.dependencies.map((dep) => dep ? Container.resolve(dep) : void 0);
|
|
2080
|
+
const instance = new target(...args);
|
|
2081
|
+
if (typeof instance.onInit === "function") {
|
|
2082
|
+
instance.onInit();
|
|
2083
|
+
}
|
|
2084
|
+
if (meta.scope === "singleton") {
|
|
2085
|
+
this.services.set(target, instance);
|
|
2086
|
+
}
|
|
2087
|
+
return instance;
|
|
2088
|
+
} finally {
|
|
2089
|
+
this.resolvingStack.delete(target);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
static async teardown() {
|
|
2093
|
+
for (const [target, instance] of this.services.entries()) {
|
|
2094
|
+
if (typeof instance.onDestroy === "function") {
|
|
2095
|
+
await instance.onDestroy();
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
this.services.clear();
|
|
2099
|
+
this.cache.clear();
|
|
1731
2100
|
}
|
|
1732
2101
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
}
|
|
1737
|
-
function Inject(token) {
|
|
1738
|
-
return (target, key) => {
|
|
1739
|
-
Object.defineProperty(target, key, {
|
|
1740
|
-
get: () => Container.resolve(token),
|
|
1741
|
-
enumerable: true,
|
|
1742
|
-
configurable: true
|
|
1743
|
-
});
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
2102
|
+
const di = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
2103
|
+
__proto__: null,
|
|
2104
|
+
Container
|
|
2105
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1746
2106
|
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1747
2107
|
function traceHandler(fn, name) {
|
|
1748
2108
|
return async function(...args) {
|
|
@@ -1805,6 +2165,7 @@ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
|
1805
2165
|
RouteParamType2["HEADER"] = "HEADER";
|
|
1806
2166
|
RouteParamType2["REQUEST"] = "REQUEST";
|
|
1807
2167
|
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
2168
|
+
RouteParamType2["SERVICE"] = "SERVICE";
|
|
1808
2169
|
return RouteParamType2;
|
|
1809
2170
|
})(RouteParamType || {});
|
|
1810
2171
|
class ControllerScanner {
|
|
@@ -1836,7 +2197,7 @@ class ControllerScanner {
|
|
|
1836
2197
|
line: info.line,
|
|
1837
2198
|
name: instance.constructor.name
|
|
1838
2199
|
};
|
|
1839
|
-
router.
|
|
2200
|
+
router.bindController(instance);
|
|
1840
2201
|
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1841
2202
|
const proto = Object.getPrototypeOf(instance);
|
|
1842
2203
|
const methods = /* @__PURE__ */ new Set();
|
|
@@ -1958,6 +2319,9 @@ class ControllerScanner {
|
|
|
1958
2319
|
case RouteParamType.CONTEXT:
|
|
1959
2320
|
args[arg.index] = ctx;
|
|
1960
2321
|
break;
|
|
2322
|
+
case RouteParamType.SERVICE:
|
|
2323
|
+
args[arg.index] = Container.resolve(arg.token);
|
|
2324
|
+
break;
|
|
1961
2325
|
}
|
|
1962
2326
|
}
|
|
1963
2327
|
}
|
|
@@ -2147,6 +2511,15 @@ class ShokupanRequestBase {
|
|
|
2147
2511
|
this.headers = new Headers(this.headers);
|
|
2148
2512
|
}
|
|
2149
2513
|
}
|
|
2514
|
+
clone() {
|
|
2515
|
+
return new ShokupanRequest({
|
|
2516
|
+
method: this.method,
|
|
2517
|
+
url: this.url,
|
|
2518
|
+
headers: new Headers(this.headers),
|
|
2519
|
+
body: this.body
|
|
2520
|
+
// Shallow copy of body, might need deep copy if object
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2150
2523
|
}
|
|
2151
2524
|
const ShokupanRequest = ShokupanRequestBase;
|
|
2152
2525
|
class RouterTrie {
|
|
@@ -2312,9 +2685,6 @@ class ShokupanRouter {
|
|
|
2312
2685
|
return this._hasAfterValidateHook;
|
|
2313
2686
|
}
|
|
2314
2687
|
requestTimeout;
|
|
2315
|
-
get db() {
|
|
2316
|
-
return this.root?.db;
|
|
2317
|
-
}
|
|
2318
2688
|
hookCache = /* @__PURE__ */ new Map();
|
|
2319
2689
|
hooksInitialized = false;
|
|
2320
2690
|
middleware = [];
|
|
@@ -2340,7 +2710,7 @@ class ShokupanRouter {
|
|
|
2340
2710
|
return this;
|
|
2341
2711
|
}
|
|
2342
2712
|
// Registry Accessor
|
|
2343
|
-
|
|
2713
|
+
get registry() {
|
|
2344
2714
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
2345
2715
|
const localRoutes = [];
|
|
2346
2716
|
for (let i = 0; i < this[$routes].length; i++) {
|
|
@@ -2376,7 +2746,7 @@ class ShokupanRouter {
|
|
|
2376
2746
|
type: "router",
|
|
2377
2747
|
path: r[$mountPath].startsWith("/") ? r[$mountPath] : "/" + r[$mountPath],
|
|
2378
2748
|
metadata: r.metadata,
|
|
2379
|
-
children: r.
|
|
2749
|
+
children: r.registry
|
|
2380
2750
|
}));
|
|
2381
2751
|
const controllers = this[$childControllers].map((c) => {
|
|
2382
2752
|
const routes = controllerRoutesMap.get(c) || [];
|
|
@@ -2471,7 +2841,7 @@ class ShokupanRouter {
|
|
|
2471
2841
|
/**
|
|
2472
2842
|
* Registers a controller instance to the router.
|
|
2473
2843
|
*/
|
|
2474
|
-
|
|
2844
|
+
bindController(controller) {
|
|
2475
2845
|
this[$childControllers].push(controller);
|
|
2476
2846
|
}
|
|
2477
2847
|
/**
|
|
@@ -3048,68 +3418,6 @@ class ShokupanRouter {
|
|
|
3048
3418
|
}
|
|
3049
3419
|
}
|
|
3050
3420
|
}
|
|
3051
|
-
function createHttpServer() {
|
|
3052
|
-
return async (options) => {
|
|
3053
|
-
const server = http__namespace.createServer(async (req, res) => {
|
|
3054
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
3055
|
-
const request = new Request(url.toString(), {
|
|
3056
|
-
method: req.method,
|
|
3057
|
-
headers: req.headers,
|
|
3058
|
-
body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
|
|
3059
|
-
start(controller) {
|
|
3060
|
-
req.on("data", (chunk) => controller.enqueue(chunk));
|
|
3061
|
-
req.on("end", () => controller.close());
|
|
3062
|
-
req.on("error", (err) => controller.error(err));
|
|
3063
|
-
}
|
|
3064
|
-
}),
|
|
3065
|
-
// Required for Node.js undici when sending a body
|
|
3066
|
-
duplex: "half"
|
|
3067
|
-
});
|
|
3068
|
-
const response = await options.fetch(request, fauxServer);
|
|
3069
|
-
res.statusCode = response.status;
|
|
3070
|
-
response.headers.forEach((v, k) => res.setHeader(k, v));
|
|
3071
|
-
if (response.body) {
|
|
3072
|
-
const buffer = await response.arrayBuffer();
|
|
3073
|
-
res.end(Buffer.from(buffer));
|
|
3074
|
-
} else {
|
|
3075
|
-
res.end();
|
|
3076
|
-
}
|
|
3077
|
-
});
|
|
3078
|
-
const fauxServer = {
|
|
3079
|
-
stop: () => {
|
|
3080
|
-
server.close();
|
|
3081
|
-
return Promise.resolve();
|
|
3082
|
-
},
|
|
3083
|
-
upgrade(req, options2) {
|
|
3084
|
-
return false;
|
|
3085
|
-
},
|
|
3086
|
-
reload(options2) {
|
|
3087
|
-
return fauxServer;
|
|
3088
|
-
},
|
|
3089
|
-
get port() {
|
|
3090
|
-
const addr = server.address();
|
|
3091
|
-
if (typeof addr === "object" && addr !== null) {
|
|
3092
|
-
return addr.port;
|
|
3093
|
-
}
|
|
3094
|
-
return options.port;
|
|
3095
|
-
},
|
|
3096
|
-
hostname: options.hostname,
|
|
3097
|
-
development: options.development,
|
|
3098
|
-
pendingRequests: 0,
|
|
3099
|
-
requestIP: (req) => null,
|
|
3100
|
-
publish: () => 0,
|
|
3101
|
-
subscriberCount: () => 0,
|
|
3102
|
-
url: new URL(`http://${options.hostname}:${options.port}`),
|
|
3103
|
-
// Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)
|
|
3104
|
-
nodeServer: server
|
|
3105
|
-
};
|
|
3106
|
-
return new Promise((resolve) => {
|
|
3107
|
-
server.listen(options.port, options.hostname, () => {
|
|
3108
|
-
resolve(fauxServer);
|
|
3109
|
-
});
|
|
3110
|
-
});
|
|
3111
|
-
};
|
|
3112
|
-
}
|
|
3113
3421
|
class BunAdapter {
|
|
3114
3422
|
server;
|
|
3115
3423
|
async listen(port, app) {
|
|
@@ -3246,25 +3554,143 @@ class BunAdapter {
|
|
|
3246
3554
|
class NodeAdapter {
|
|
3247
3555
|
server;
|
|
3248
3556
|
async listen(port, app) {
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3557
|
+
const factory = app.applicationConfig.serverFactory;
|
|
3558
|
+
let nodeServer;
|
|
3559
|
+
if (factory) {
|
|
3560
|
+
const serveOptions = {
|
|
3561
|
+
port,
|
|
3562
|
+
hostname: app.applicationConfig.hostname,
|
|
3563
|
+
development: app.applicationConfig.development,
|
|
3564
|
+
fetch: app.fetch.bind(app),
|
|
3565
|
+
reusePort: app.applicationConfig.reusePort
|
|
3566
|
+
};
|
|
3567
|
+
this.server = await factory(serveOptions);
|
|
3568
|
+
return this.server;
|
|
3252
3569
|
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3570
|
+
nodeServer = http__namespace.createServer(async (req, res) => {
|
|
3571
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
3572
|
+
const request = new Request(url.toString(), {
|
|
3573
|
+
method: req.method,
|
|
3574
|
+
headers: req.headers,
|
|
3575
|
+
body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
|
|
3576
|
+
start(controller) {
|
|
3577
|
+
req.on("data", (chunk) => controller.enqueue(chunk));
|
|
3578
|
+
req.on("end", () => controller.close());
|
|
3579
|
+
req.on("error", (err) => controller.error(err));
|
|
3580
|
+
}
|
|
3581
|
+
}),
|
|
3582
|
+
// Required for Node.js undici when sending a body
|
|
3583
|
+
// @ts-ignore
|
|
3584
|
+
duplex: "half"
|
|
3585
|
+
});
|
|
3586
|
+
const response = await app.fetch(request, fauxServer);
|
|
3587
|
+
res.statusCode = response.status;
|
|
3588
|
+
response.headers.forEach((v, k) => res.setHeader(k, v));
|
|
3589
|
+
if (response.body) {
|
|
3590
|
+
const buffer = await response.arrayBuffer();
|
|
3591
|
+
res.end(Buffer.from(buffer));
|
|
3592
|
+
} else {
|
|
3593
|
+
res.end();
|
|
3594
|
+
}
|
|
3595
|
+
});
|
|
3596
|
+
this.server = nodeServer;
|
|
3597
|
+
const fauxServer = {
|
|
3598
|
+
stop: () => {
|
|
3599
|
+
nodeServer.close();
|
|
3600
|
+
return Promise.resolve();
|
|
3601
|
+
},
|
|
3602
|
+
upgrade(req, options) {
|
|
3603
|
+
return false;
|
|
3604
|
+
},
|
|
3605
|
+
reload(options) {
|
|
3606
|
+
return fauxServer;
|
|
3607
|
+
},
|
|
3608
|
+
get port() {
|
|
3609
|
+
const addr = nodeServer.address();
|
|
3610
|
+
if (typeof addr === "object" && addr !== null) {
|
|
3611
|
+
return addr.port;
|
|
3612
|
+
}
|
|
3613
|
+
return port;
|
|
3614
|
+
},
|
|
3615
|
+
hostname: app.applicationConfig.hostname || "localhost",
|
|
3616
|
+
development: app.applicationConfig.development || false,
|
|
3617
|
+
pendingRequests: 0,
|
|
3618
|
+
requestIP: (req) => null,
|
|
3619
|
+
publish: () => 0,
|
|
3620
|
+
subscriberCount: () => 0,
|
|
3621
|
+
url: new URL(`http://${app.applicationConfig.hostname || "localhost"}:${port}`),
|
|
3622
|
+
// Expose the raw Node.js server
|
|
3623
|
+
// @ts-ignore
|
|
3624
|
+
nodeServer
|
|
3260
3625
|
};
|
|
3261
|
-
|
|
3262
|
-
|
|
3626
|
+
return new Promise((resolve) => {
|
|
3627
|
+
nodeServer.listen(port, app.applicationConfig.hostname, () => {
|
|
3628
|
+
resolve(fauxServer);
|
|
3629
|
+
});
|
|
3630
|
+
});
|
|
3263
3631
|
}
|
|
3264
3632
|
async stop() {
|
|
3265
3633
|
if (this.server?.stop) {
|
|
3266
3634
|
await this.server.stop();
|
|
3635
|
+
} else if (this.server?.close) {
|
|
3636
|
+
this.server.close();
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
class ShokupanServer {
|
|
3641
|
+
constructor(app) {
|
|
3642
|
+
this.app = app;
|
|
3643
|
+
}
|
|
3644
|
+
server;
|
|
3645
|
+
adapter;
|
|
3646
|
+
/**
|
|
3647
|
+
* Starts the application server.
|
|
3648
|
+
* @param port The port to listen on.
|
|
3649
|
+
*/
|
|
3650
|
+
async listen(port) {
|
|
3651
|
+
const config = this.app.applicationConfig;
|
|
3652
|
+
const finalPort = port ?? config.port ?? 3e3;
|
|
3653
|
+
if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
|
|
3654
|
+
throw new Error("Invalid port number");
|
|
3655
|
+
}
|
|
3656
|
+
await this.app.start();
|
|
3657
|
+
let adapterName = config.adapter;
|
|
3658
|
+
let adapter;
|
|
3659
|
+
if (!adapterName) {
|
|
3660
|
+
if (typeof Bun !== "undefined") {
|
|
3661
|
+
config.adapter = "bun";
|
|
3662
|
+
adapter = new BunAdapter();
|
|
3663
|
+
} else {
|
|
3664
|
+
config.adapter = "node";
|
|
3665
|
+
adapter = new NodeAdapter();
|
|
3666
|
+
}
|
|
3667
|
+
} else if (adapterName === "bun") {
|
|
3668
|
+
adapter = new BunAdapter();
|
|
3669
|
+
} else if (adapterName === "node") {
|
|
3670
|
+
adapter = new NodeAdapter();
|
|
3671
|
+
} else if (adapterName === "wintercg") {
|
|
3672
|
+
throw new Error("WinterCG adapter does not support listen(). Use fetch directly.");
|
|
3673
|
+
} else {
|
|
3674
|
+
adapter = new NodeAdapter();
|
|
3675
|
+
}
|
|
3676
|
+
this.adapter = adapter;
|
|
3677
|
+
this.app.compile();
|
|
3678
|
+
this.server = await adapter.listen(finalPort, this.app);
|
|
3679
|
+
if (finalPort === 0 && this.server?.port) {
|
|
3680
|
+
config.port = this.server.port;
|
|
3267
3681
|
}
|
|
3682
|
+
return this.server;
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* Stops the server.
|
|
3686
|
+
*/
|
|
3687
|
+
async stop(closeActiveConnections) {
|
|
3688
|
+
if (this.adapter?.stop) {
|
|
3689
|
+
await this.adapter.stop();
|
|
3690
|
+
} else if (this.server?.stop) {
|
|
3691
|
+
await this.server.stop(closeActiveConnections);
|
|
3692
|
+
}
|
|
3693
|
+
this.server = void 0;
|
|
3268
3694
|
}
|
|
3269
3695
|
}
|
|
3270
3696
|
let fs;
|
|
@@ -3455,6 +3881,7 @@ const defaults = {
|
|
|
3455
3881
|
development: process.env.NODE_ENV !== "production",
|
|
3456
3882
|
enableAsyncLocalStorage: false,
|
|
3457
3883
|
enableHttpBridge: false,
|
|
3884
|
+
enableOpenApiGen: true,
|
|
3458
3885
|
reusePort: false
|
|
3459
3886
|
};
|
|
3460
3887
|
class Shokupan extends ShokupanRouter {
|
|
@@ -3466,8 +3893,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
3466
3893
|
composedMiddleware;
|
|
3467
3894
|
cpuMonitor;
|
|
3468
3895
|
server;
|
|
3896
|
+
httpServer;
|
|
3469
3897
|
datastore;
|
|
3470
3898
|
dbPromise;
|
|
3899
|
+
// Performance: Flattened Router Trie
|
|
3900
|
+
rootTrie;
|
|
3471
3901
|
get db() {
|
|
3472
3902
|
return this.datastore;
|
|
3473
3903
|
}
|
|
@@ -3488,6 +3918,10 @@ class Shokupan extends ShokupanRouter {
|
|
|
3488
3918
|
line,
|
|
3489
3919
|
name: "ShokupanApplication"
|
|
3490
3920
|
};
|
|
3921
|
+
if (this.applicationConfig.securityHeaders !== false) {
|
|
3922
|
+
const { SecurityHeaders: SecurityHeaders2 } = require("./plugins/middleware/security-headers");
|
|
3923
|
+
this.use(SecurityHeaders2(this.applicationConfig.securityHeaders === true ? {} : this.applicationConfig.securityHeaders));
|
|
3924
|
+
}
|
|
3491
3925
|
if (this.applicationConfig.adapter !== "wintercg") {
|
|
3492
3926
|
this.dbPromise = this.initDatastore().catch((err) => {
|
|
3493
3927
|
this.logger?.debug("Failed to initialize default datastore", { error: err });
|
|
@@ -3568,11 +4002,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
3568
4002
|
* @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.
|
|
3569
4003
|
* @returns The server instance.
|
|
3570
4004
|
*/
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
4005
|
+
/**
|
|
4006
|
+
* Prepare the application for listening.
|
|
4007
|
+
* Use this if you want to initialize the app without starting the server immediately.
|
|
4008
|
+
*/
|
|
4009
|
+
async start() {
|
|
3576
4010
|
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
3577
4011
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
3578
4012
|
this.get("/.well-known/openapi.yaml", async (ctx) => {
|
|
@@ -3603,13 +4037,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
3603
4037
|
auth: config.auth || { type: "none" },
|
|
3604
4038
|
api: config.api || {
|
|
3605
4039
|
type: "openapi",
|
|
3606
|
-
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
4040
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/.well-known/openapi.yaml`,
|
|
3607
4041
|
is_user_authenticated: false
|
|
3608
4042
|
},
|
|
3609
|
-
logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
4043
|
+
logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/logo.png`,
|
|
3610
4044
|
// Placeholder default
|
|
3611
4045
|
contact_email: config.contact_email || pkg.author?.email || "support@example.com",
|
|
3612
|
-
legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
4046
|
+
legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/legal`
|
|
3613
4047
|
};
|
|
3614
4048
|
return ctx.json(manifest);
|
|
3615
4049
|
});
|
|
@@ -3622,8 +4056,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
3622
4056
|
versions: config.versions || [
|
|
3623
4057
|
{
|
|
3624
4058
|
name: this.openApiSpec.info.version || "v1",
|
|
3625
|
-
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
3626
|
-
spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${
|
|
4059
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/`,
|
|
4060
|
+
spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/.well-known/openapi.yaml`
|
|
3627
4061
|
}
|
|
3628
4062
|
]
|
|
3629
4063
|
};
|
|
@@ -3659,28 +4093,20 @@ class Shokupan extends ShokupanRouter {
|
|
|
3659
4093
|
await this.asyncApiSpecPromise;
|
|
3660
4094
|
}
|
|
3661
4095
|
}
|
|
3662
|
-
if (port === 0 && process.platform === "linux") ;
|
|
3663
4096
|
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
3664
4097
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
3665
4098
|
this.cpuMonitor.start();
|
|
3666
4099
|
}
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
adapter = new BunAdapter();
|
|
3678
|
-
} else if (adapter === "node") {
|
|
3679
|
-
adapter = new NodeAdapter();
|
|
3680
|
-
} else if (adapter === "wintercg") {
|
|
3681
|
-
throw new Error("WinterCG adapter does not support listen(). Use fetch directly.");
|
|
3682
|
-
}
|
|
3683
|
-
this.server = await adapter.listen(finalPort, this);
|
|
4100
|
+
}
|
|
4101
|
+
/**
|
|
4102
|
+
* Starts the application server.
|
|
4103
|
+
*
|
|
4104
|
+
* @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.
|
|
4105
|
+
* @returns The server instance.
|
|
4106
|
+
*/
|
|
4107
|
+
async listen(port) {
|
|
4108
|
+
this.httpServer = new ShokupanServer(this);
|
|
4109
|
+
this.server = await this.httpServer.listen(port);
|
|
3684
4110
|
return this.server;
|
|
3685
4111
|
}
|
|
3686
4112
|
/**
|
|
@@ -3706,10 +4132,14 @@ class Shokupan extends ShokupanRouter {
|
|
|
3706
4132
|
this.cpuMonitor.stop();
|
|
3707
4133
|
this.cpuMonitor = void 0;
|
|
3708
4134
|
}
|
|
3709
|
-
if (this.
|
|
4135
|
+
if (this.httpServer !== void 0) {
|
|
4136
|
+
await this.httpServer.stop(closeActiveConnections);
|
|
4137
|
+
} else if (this.server?.stop) {
|
|
3710
4138
|
await this.server.stop(closeActiveConnections);
|
|
3711
|
-
this.server = void 0;
|
|
3712
4139
|
}
|
|
4140
|
+
this.server = void 0;
|
|
4141
|
+
const { Container: Container2 } = await Promise.resolve().then(() => di);
|
|
4142
|
+
await Container2.teardown();
|
|
3713
4143
|
}
|
|
3714
4144
|
[$dispatch](req) {
|
|
3715
4145
|
return this.fetch(req);
|
|
@@ -3718,6 +4148,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
3718
4148
|
* Processes a request by wrapping the standard fetch method.
|
|
3719
4149
|
*/
|
|
3720
4150
|
async testRequest(options) {
|
|
4151
|
+
if (!this.rootTrie) {
|
|
4152
|
+
this.compile();
|
|
4153
|
+
}
|
|
3721
4154
|
let url = options.url || options.path || "/";
|
|
3722
4155
|
if (!url.startsWith("http")) {
|
|
3723
4156
|
const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
|
|
@@ -3833,7 +4266,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
3833
4266
|
} else if (ctx.isUpgraded) {
|
|
3834
4267
|
return void 0;
|
|
3835
4268
|
} else if (ctx[$routeMatched]) {
|
|
3836
|
-
|
|
4269
|
+
let status = ctx.response.status;
|
|
4270
|
+
if (status === HTTP_STATUS.OK) {
|
|
4271
|
+
status = HTTP_STATUS.NO_CONTENT;
|
|
4272
|
+
}
|
|
4273
|
+
response = ctx.send(null, { status, headers: ctx.response.headers });
|
|
3837
4274
|
} else {
|
|
3838
4275
|
if (ctx.upgrade()) {
|
|
3839
4276
|
return void 0;
|
|
@@ -3862,8 +4299,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
3862
4299
|
if (err instanceof SyntaxError && err.message.includes("JSON")) {
|
|
3863
4300
|
status = 400;
|
|
3864
4301
|
}
|
|
3865
|
-
const
|
|
3866
|
-
|
|
4302
|
+
const isDev = this.applicationConfig.development !== false;
|
|
4303
|
+
const message = isDev ? err.message || "Internal Server Error" : "Internal Server Error";
|
|
4304
|
+
const body = { error: message };
|
|
4305
|
+
if (isDev && err.errors) body.errors = err.errors;
|
|
4306
|
+
if (isDev && err.stack) body.stack = err.stack;
|
|
3867
4307
|
if (this.hasOnErrorHook) await this.runHooks("onError", ctx, err);
|
|
3868
4308
|
return ctx.json(body, status);
|
|
3869
4309
|
}
|
|
@@ -3892,6 +4332,72 @@ class Shokupan extends ShokupanRouter {
|
|
|
3892
4332
|
return res;
|
|
3893
4333
|
});
|
|
3894
4334
|
}
|
|
4335
|
+
/**
|
|
4336
|
+
* Compiles all routes into a master Trie for O(1) router lookup.
|
|
4337
|
+
* Use this if adding routes dynamically after start (not recommended but possible).
|
|
4338
|
+
*/
|
|
4339
|
+
compile() {
|
|
4340
|
+
this.rootTrie = new RouterTrie();
|
|
4341
|
+
this.flattenRoutes(this.rootTrie, this, "", []);
|
|
4342
|
+
}
|
|
4343
|
+
flattenRoutes(trie, router, prefix, middlewareStack) {
|
|
4344
|
+
let effectiveStack = middlewareStack;
|
|
4345
|
+
if (router !== this) {
|
|
4346
|
+
effectiveStack = [...middlewareStack, ...router.middleware];
|
|
4347
|
+
}
|
|
4348
|
+
const joinPath = (base, segment) => {
|
|
4349
|
+
let b = base;
|
|
4350
|
+
if (b !== "/" && b.endsWith("/")) {
|
|
4351
|
+
b = b.slice(0, -1);
|
|
4352
|
+
}
|
|
4353
|
+
let s = segment;
|
|
4354
|
+
if (s === "/") {
|
|
4355
|
+
return b;
|
|
4356
|
+
}
|
|
4357
|
+
if (s === "") {
|
|
4358
|
+
return b;
|
|
4359
|
+
}
|
|
4360
|
+
if (!s.startsWith("/")) {
|
|
4361
|
+
s = "/" + s;
|
|
4362
|
+
}
|
|
4363
|
+
if (b === "/") {
|
|
4364
|
+
return s;
|
|
4365
|
+
}
|
|
4366
|
+
return b + s;
|
|
4367
|
+
};
|
|
4368
|
+
for (const route of router[$routes]) {
|
|
4369
|
+
const fullPath = joinPath(prefix, route.path);
|
|
4370
|
+
let handler = route.bakedHandler || route.handler;
|
|
4371
|
+
if (effectiveStack.length > 0) {
|
|
4372
|
+
const fn = compose(effectiveStack);
|
|
4373
|
+
const originalHandler = handler;
|
|
4374
|
+
handler = async (ctx) => {
|
|
4375
|
+
return fn(ctx, () => originalHandler(ctx));
|
|
4376
|
+
};
|
|
4377
|
+
handler.originalHandler = originalHandler.originalHandler || originalHandler;
|
|
4378
|
+
}
|
|
4379
|
+
trie.insert(route.method, fullPath, handler);
|
|
4380
|
+
if ((route.path === "/" || route.path === "") && fullPath !== "/") {
|
|
4381
|
+
trie.insert(route.method, fullPath + "/", handler);
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
for (const child of router[$childRouters]) {
|
|
4385
|
+
const mountPath = child[$mountPath];
|
|
4386
|
+
const childPrefix = joinPath(prefix, mountPath);
|
|
4387
|
+
this.flattenRoutes(trie, child, childPrefix, effectiveStack);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
find(method, path2) {
|
|
4391
|
+
if (this.rootTrie) {
|
|
4392
|
+
const result = this.rootTrie.search(method, path2);
|
|
4393
|
+
if (result) return result;
|
|
4394
|
+
if (method === "HEAD") {
|
|
4395
|
+
return this.rootTrie.search("GET", path2);
|
|
4396
|
+
}
|
|
4397
|
+
return null;
|
|
4398
|
+
}
|
|
4399
|
+
return super.find(method, path2);
|
|
4400
|
+
}
|
|
3895
4401
|
}
|
|
3896
4402
|
function RateLimitMiddleware(options = {}) {
|
|
3897
4403
|
const windowMs = options.windowMs || 60 * 1e3;
|
|
@@ -3901,6 +4407,7 @@ function RateLimitMiddleware(options = {}) {
|
|
|
3901
4407
|
const headers = options.headers !== false;
|
|
3902
4408
|
const mode = options.mode || "user";
|
|
3903
4409
|
const trustedProxies = options.trustedProxies || [];
|
|
4410
|
+
const cleanupInterval = options.cleanupInterval || windowMs;
|
|
3904
4411
|
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
3905
4412
|
if (mode === "absolute") {
|
|
3906
4413
|
return "global";
|
|
@@ -3930,7 +4437,7 @@ function RateLimitMiddleware(options = {}) {
|
|
|
3930
4437
|
hits.delete(key);
|
|
3931
4438
|
}
|
|
3932
4439
|
}
|
|
3933
|
-
},
|
|
4440
|
+
}, cleanupInterval);
|
|
3934
4441
|
if (interval.unref) interval.unref();
|
|
3935
4442
|
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
3936
4443
|
if (skip(ctx)) return next();
|
|
@@ -3987,8 +4494,79 @@ function Controller(path2 = "/") {
|
|
|
3987
4494
|
target[$controllerPath] = path2;
|
|
3988
4495
|
};
|
|
3989
4496
|
}
|
|
3990
|
-
function
|
|
3991
|
-
return (target
|
|
4497
|
+
function Injectable(scope = "singleton") {
|
|
4498
|
+
return (target) => {
|
|
4499
|
+
Reflect.defineMetadata("di:scope", scope, target);
|
|
4500
|
+
};
|
|
4501
|
+
}
|
|
4502
|
+
function Inject(token) {
|
|
4503
|
+
return (target, propertyKey, indexOrDescriptor) => {
|
|
4504
|
+
if (typeof indexOrDescriptor === "undefined" || typeof indexOrDescriptor === "object" && indexOrDescriptor !== null) {
|
|
4505
|
+
const key = String(propertyKey);
|
|
4506
|
+
Object.defineProperty(target, key, {
|
|
4507
|
+
get: () => Container.resolve(token),
|
|
4508
|
+
enumerable: true,
|
|
4509
|
+
configurable: true
|
|
4510
|
+
});
|
|
4511
|
+
return;
|
|
4512
|
+
}
|
|
4513
|
+
if (typeof indexOrDescriptor === "number") {
|
|
4514
|
+
const index = indexOrDescriptor;
|
|
4515
|
+
const existing = Reflect.getMetadata("di:constructor:params", target) || [];
|
|
4516
|
+
existing.push({ index, token });
|
|
4517
|
+
Reflect.defineMetadata("di:constructor:params", existing, target);
|
|
4518
|
+
}
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
function Use(tokenOrMiddleware, ...moreMiddleware) {
|
|
4522
|
+
return (target, propertyKey, indexOrDescriptor) => {
|
|
4523
|
+
if (typeof indexOrDescriptor === "number") {
|
|
4524
|
+
const index = indexOrDescriptor;
|
|
4525
|
+
if (!propertyKey) {
|
|
4526
|
+
let token2 = tokenOrMiddleware;
|
|
4527
|
+
if (!token2) {
|
|
4528
|
+
const paramTypes = Reflect.getMetadata("design:paramtypes", target);
|
|
4529
|
+
if (paramTypes && paramTypes[index]) {
|
|
4530
|
+
token2 = paramTypes[index];
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
const existing = Reflect.getMetadata("di:constructor:params", target) || [];
|
|
4534
|
+
existing.push({ index, token: token2 });
|
|
4535
|
+
Reflect.defineMetadata("di:constructor:params", existing, target);
|
|
4536
|
+
return;
|
|
4537
|
+
}
|
|
4538
|
+
if (!target[$routeArgs]) target[$routeArgs] = /* @__PURE__ */ new Map();
|
|
4539
|
+
if (!target[$routeArgs].has(propertyKey)) target[$routeArgs].set(propertyKey, []);
|
|
4540
|
+
let token = tokenOrMiddleware;
|
|
4541
|
+
if (!token) {
|
|
4542
|
+
const paramTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey);
|
|
4543
|
+
if (paramTypes && paramTypes[index]) {
|
|
4544
|
+
token = paramTypes[index];
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
target[$routeArgs].get(propertyKey).push({
|
|
4548
|
+
index,
|
|
4549
|
+
type: RouteParamType.SERVICE,
|
|
4550
|
+
token
|
|
4551
|
+
});
|
|
4552
|
+
return;
|
|
4553
|
+
}
|
|
4554
|
+
if (typeof propertyKey === "string" && indexOrDescriptor === void 0) {
|
|
4555
|
+
let token = tokenOrMiddleware;
|
|
4556
|
+
if (!token) {
|
|
4557
|
+
token = Reflect.getMetadata("design:type", target, propertyKey);
|
|
4558
|
+
}
|
|
4559
|
+
Object.defineProperty(target, propertyKey, {
|
|
4560
|
+
get: () => {
|
|
4561
|
+
if (!token) throw new Error(`Cannot resolve dependency for ${target.constructor.name}.${propertyKey} - no token provided and types unavailable.`);
|
|
4562
|
+
return Container.resolve(token);
|
|
4563
|
+
},
|
|
4564
|
+
enumerable: true,
|
|
4565
|
+
configurable: true
|
|
4566
|
+
});
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
const middleware = [tokenOrMiddleware, ...moreMiddleware];
|
|
3992
4570
|
if (!propertyKey) {
|
|
3993
4571
|
const existing = target[$middleware] || [];
|
|
3994
4572
|
target[$middleware] = [...existing, ...middleware];
|
|
@@ -4068,7 +4646,7 @@ function Event(eventName) {
|
|
|
4068
4646
|
function RateLimit(options) {
|
|
4069
4647
|
return Use(RateLimitMiddleware(options));
|
|
4070
4648
|
}
|
|
4071
|
-
function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
4649
|
+
function ApiExplorerApp({ spec, base, asyncSpec, config }) {
|
|
4072
4650
|
const hierarchy = /* @__PURE__ */ new Map();
|
|
4073
4651
|
const addRoute = (groupKey, route) => {
|
|
4074
4652
|
if (!hierarchy.has(groupKey)) {
|
|
@@ -4254,8 +4832,8 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
|
4254
4832
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
4255
4833
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
|
|
4256
4834
|
/* @__PURE__ */ jsxRuntime.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" }),
|
|
4257
|
-
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href:
|
|
4258
|
-
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href:
|
|
4835
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
|
|
4836
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
4259
4837
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
|
|
4260
4838
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
|
|
4261
4839
|
/* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
|
|
@@ -4275,7 +4853,7 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
|
4275
4853
|
/* @__PURE__ */ jsxRuntime.jsx(Sidebar$1, { spec, hierarchicalGroups }),
|
|
4276
4854
|
/* @__PURE__ */ jsxRuntime.jsx(MainContent$1, { allRoutes, config, spec })
|
|
4277
4855
|
] }),
|
|
4278
|
-
/* @__PURE__ */ jsxRuntime.jsx("script", { src:
|
|
4856
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/explorer-client.mjs`, type: "module" })
|
|
4279
4857
|
] })
|
|
4280
4858
|
] });
|
|
4281
4859
|
}
|
|
@@ -4461,8 +5039,14 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
4461
5039
|
this.get("/_source", async (ctx) => {
|
|
4462
5040
|
const file = ctx.query["file"];
|
|
4463
5041
|
if (!file) return ctx.text("Missing file parameter", 400);
|
|
5042
|
+
const { resolve, normalize, isAbsolute } = await import("node:path");
|
|
5043
|
+
const cwd = process.cwd();
|
|
5044
|
+
const resolvedPath = resolve(cwd, file);
|
|
5045
|
+
if (!resolvedPath.startsWith(cwd)) {
|
|
5046
|
+
return ctx.text("Forbidden: File must be within project root", 403);
|
|
5047
|
+
}
|
|
4464
5048
|
try {
|
|
4465
|
-
const content = await promises$1.readFile(
|
|
5049
|
+
const content = await promises$1.readFile(resolvedPath, "utf-8");
|
|
4466
5050
|
return ctx.text(content);
|
|
4467
5051
|
} catch (err) {
|
|
4468
5052
|
return ctx.text("File not found", 404);
|
|
@@ -4475,7 +5059,8 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
4475
5059
|
this.get("/", async (ctx) => {
|
|
4476
5060
|
const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
|
|
4477
5061
|
const asyncSpec = ctx.app.asyncApiSpec;
|
|
4478
|
-
const
|
|
5062
|
+
const base = this.pluginOptions.path;
|
|
5063
|
+
const element = ApiExplorerApp({ spec: stripSourceCode(spec), base, asyncSpec });
|
|
4479
5064
|
const html = renderToString(element);
|
|
4480
5065
|
if (html.length === 0) throw new Error("ApiExplorerPlugin: rendered page is blank.");
|
|
4481
5066
|
return ctx.html(html);
|
|
@@ -4517,12 +5102,12 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
|
4517
5102
|
] });
|
|
4518
5103
|
}
|
|
4519
5104
|
function Sidebar({ navTree, disableSourceView }) {
|
|
4520
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar
|
|
5105
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar", id: "sidebar", children: [
|
|
4521
5106
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
|
|
4522
5107
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "AsyncAPI" }),
|
|
4523
5108
|
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-collapse-nav", class: "btn-icon", title: "Collapse Sidebar", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "15 18 9 12 15 6" }) }) })
|
|
4524
5109
|
] }),
|
|
4525
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
|
|
5110
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-list scroller", id: "nav-list", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
|
|
4526
5111
|
] });
|
|
4527
5112
|
}
|
|
4528
5113
|
function NavNode({ node, level, disableSourceView }) {
|
|
@@ -4692,7 +5277,7 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4692
5277
|
let astMiddlewareRegistry = {};
|
|
4693
5278
|
let applications = [];
|
|
4694
5279
|
try {
|
|
4695
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-
|
|
5280
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-DM-OlRq8.cjs"));
|
|
4696
5281
|
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
4697
5282
|
const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
4698
5283
|
const analysisResult = await analyzer2.analyze();
|
|
@@ -5102,8 +5687,14 @@ class AsyncApiPlugin extends ShokupanRouter {
|
|
|
5102
5687
|
if (!file || typeof file !== "string") {
|
|
5103
5688
|
return ctx.text("Missing file parameter", 400);
|
|
5104
5689
|
}
|
|
5690
|
+
const { resolve } = await import("node:path");
|
|
5691
|
+
const cwd = process.cwd();
|
|
5692
|
+
const resolvedPath = resolve(cwd, file);
|
|
5693
|
+
if (!resolvedPath.startsWith(cwd)) {
|
|
5694
|
+
return ctx.text("Forbidden: File must be within project root", 403);
|
|
5695
|
+
}
|
|
5105
5696
|
try {
|
|
5106
|
-
const content = await promises.readFile(
|
|
5697
|
+
const content = await promises.readFile(resolvedPath, "utf8");
|
|
5107
5698
|
return ctx.text(content);
|
|
5108
5699
|
} catch (e) {
|
|
5109
5700
|
return ctx.text("File not found: " + e.message, 404);
|
|
@@ -5158,7 +5749,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
5158
5749
|
}
|
|
5159
5750
|
}
|
|
5160
5751
|
async createSession(user, ctx) {
|
|
5161
|
-
const alg = "HS256";
|
|
5752
|
+
const alg = this.authConfig.jwtAlgorithm || "HS256";
|
|
5162
5753
|
const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
5163
5754
|
const opts = this.authConfig.cookieOptions || {};
|
|
5164
5755
|
let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
|
|
@@ -5460,7 +6051,7 @@ class ClusterPlugin {
|
|
|
5460
6051
|
}
|
|
5461
6052
|
}
|
|
5462
6053
|
}
|
|
5463
|
-
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern }) {
|
|
6054
|
+
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern, ignorePaths }) {
|
|
5464
6055
|
return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
|
|
5465
6056
|
/* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
|
|
5466
6057
|
/* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
|
|
@@ -5570,22 +6161,22 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
|
|
|
5570
6161
|
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "ws", children: "WS" }),
|
|
5571
6162
|
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "other", children: "Other" })
|
|
5572
6163
|
] }),
|
|
6164
|
+
/* @__PURE__ */ jsxRuntime.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: [
|
|
6165
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", id: "network-filter-ignore", checked: true }),
|
|
6166
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { for: "network-filter-ignore", style: "cursor: pointer; font-size: 0.9em; user-select: none;", children: "Excl. Ignored" })
|
|
6167
|
+
] }),
|
|
5573
6168
|
/* @__PURE__ */ jsxRuntime.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" }),
|
|
5574
6169
|
/* @__PURE__ */ jsxRuntime.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" })
|
|
5575
6170
|
] }) }),
|
|
5576
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "network-view", class: "active", style: "display: block; height:
|
|
6171
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "network-view", class: "active", style: "display: block; height: 100%; margin-bottom: 2rem; overflow: hidden;", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "margin: 0 2rem; display: flex; gap: 1rem; height: 100%;", children: [
|
|
5577
6172
|
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-list-container", style: "flex: 1; height: 100%; border-radius: 6px; overflow: hidden; border: 1px solid var(--card-border);" }),
|
|
5578
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "request-details-container", class: "card", style: "display: none; width: 500px; height: 100%; overflow
|
|
6173
|
+
/* @__PURE__ */ jsxRuntime.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: [
|
|
5579
6174
|
/* @__PURE__ */ jsxRuntime.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;" }),
|
|
5580
6175
|
/* @__PURE__ */ jsxRuntime.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: [
|
|
5581
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin: 0;", children: "Request Details" }),
|
|
6176
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin: 0; padding: 0", children: "Request Details" }),
|
|
5582
6177
|
/* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "closeRequestDetails()", style: "background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.2rem;", children: "×" })
|
|
5583
6178
|
] }),
|
|
5584
|
-
/* @__PURE__ */ jsxRuntime.
|
|
5585
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "request-details-content" }),
|
|
5586
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
|
|
5587
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "middleware-trace-container" })
|
|
5588
|
-
] })
|
|
6179
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; flex-direction: column; overflow: hidden; height: 100%", children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "request-details-content", style: "flex: 1; display: flex; flex-direction: column; height: 100%; overflow: hidden" }) })
|
|
5589
6180
|
] })
|
|
5590
6181
|
] }) })
|
|
5591
6182
|
] }),
|
|
@@ -5600,7 +6191,8 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
|
|
|
5600
6191
|
const getRequestHeaders = ${getRequestHeadersSource};
|
|
5601
6192
|
window.SHOKUPAN_CONFIG = {
|
|
5602
6193
|
rootPath: "${rootPath || ""}",
|
|
5603
|
-
linkPattern: "${linkPattern || ""}"
|
|
6194
|
+
linkPattern: "${linkPattern || ""}",
|
|
6195
|
+
ignorePaths: ${JSON.stringify(ignorePaths || [])}
|
|
5604
6196
|
};
|
|
5605
6197
|
`
|
|
5606
6198
|
} }),
|
|
@@ -5609,7 +6201,6 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
|
|
|
5609
6201
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/charts.js` }),
|
|
5610
6202
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tables.js` }),
|
|
5611
6203
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/registry.js` }),
|
|
5612
|
-
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/failures.js` }),
|
|
5613
6204
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/requests.js` }),
|
|
5614
6205
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tabs.js` })
|
|
5615
6206
|
] })
|
|
@@ -5678,15 +6269,51 @@ const require$1 = node_module.createRequire(typeof document === "undefined" ? re
|
|
|
5678
6269
|
const http = require$1("node:http");
|
|
5679
6270
|
const https = require$1("node:https");
|
|
5680
6271
|
class FetchInterceptor {
|
|
6272
|
+
static originalFetch;
|
|
6273
|
+
static originalHttpRequest;
|
|
6274
|
+
static originalHttpsRequest;
|
|
5681
6275
|
originalFetch;
|
|
5682
6276
|
originalHttpRequest;
|
|
5683
6277
|
originalHttpsRequest;
|
|
5684
6278
|
callbacks = [];
|
|
5685
6279
|
isPatched = false;
|
|
5686
6280
|
constructor() {
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
6281
|
+
if (!FetchInterceptor.originalFetch) {
|
|
6282
|
+
if (global.fetch.__isPatched) {
|
|
6283
|
+
console.warn("[FetchInterceptor] Global fetch is already patched! Cannot capture original.");
|
|
6284
|
+
} else {
|
|
6285
|
+
FetchInterceptor.originalFetch = global.fetch;
|
|
6286
|
+
FetchInterceptor.originalHttpRequest = http.request;
|
|
6287
|
+
FetchInterceptor.originalHttpsRequest = https.request;
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
this.originalFetch = FetchInterceptor.originalFetch || global.fetch;
|
|
6291
|
+
this.originalHttpRequest = FetchInterceptor.originalHttpRequest || http.request;
|
|
6292
|
+
this.originalHttpsRequest = FetchInterceptor.originalHttpsRequest || https.request;
|
|
6293
|
+
}
|
|
6294
|
+
/**
|
|
6295
|
+
* Statically restore the original network methods.
|
|
6296
|
+
* Useful for cleaning up in tests.
|
|
6297
|
+
*/
|
|
6298
|
+
/**
|
|
6299
|
+
* Statically restore the original network methods.
|
|
6300
|
+
* Useful for cleaning up in tests.
|
|
6301
|
+
*/
|
|
6302
|
+
static restore() {
|
|
6303
|
+
if (FetchInterceptor.originalFetch) {
|
|
6304
|
+
global.fetch = FetchInterceptor.originalFetch;
|
|
6305
|
+
} else if (global.fetch?.__originalFetch) {
|
|
6306
|
+
global.fetch = global.fetch.__originalFetch;
|
|
6307
|
+
} else if (typeof Bun !== "undefined" && Bun.fetch) {
|
|
6308
|
+
global.fetch = Bun.fetch;
|
|
6309
|
+
}
|
|
6310
|
+
if (FetchInterceptor.originalHttpRequest) {
|
|
6311
|
+
http.request = FetchInterceptor.originalHttpRequest;
|
|
6312
|
+
}
|
|
6313
|
+
if (FetchInterceptor.originalHttpsRequest) {
|
|
6314
|
+
https.request = FetchInterceptor.originalHttpsRequest;
|
|
6315
|
+
}
|
|
6316
|
+
console.log("[FetchInterceptor] Network layer restored (static).");
|
|
5690
6317
|
}
|
|
5691
6318
|
/**
|
|
5692
6319
|
* Patches the global `fetch` function to intercept requests.
|
|
@@ -5701,37 +6328,33 @@ class FetchInterceptor {
|
|
|
5701
6328
|
}
|
|
5702
6329
|
patchGlobalFetch() {
|
|
5703
6330
|
const self = this;
|
|
6331
|
+
if (!this.originalFetch && global.fetch.__isPatched && global.fetch.__originalFetch) {
|
|
6332
|
+
this.originalFetch = global.fetch.__originalFetch;
|
|
6333
|
+
}
|
|
5704
6334
|
const newFetch = async function(input, init) {
|
|
5705
6335
|
const startTime = performance.now();
|
|
5706
6336
|
const timestamp = Date.now();
|
|
5707
|
-
let method = "GET";
|
|
5708
6337
|
let url = "";
|
|
6338
|
+
let method = "GET";
|
|
5709
6339
|
let requestHeaders = {};
|
|
5710
|
-
let requestBody = void 0;
|
|
5711
6340
|
try {
|
|
5712
|
-
if (input
|
|
5713
|
-
url = input.toString();
|
|
5714
|
-
} else if (typeof input === "string") {
|
|
6341
|
+
if (typeof input === "string") {
|
|
5715
6342
|
url = input;
|
|
5716
|
-
} else if (
|
|
6343
|
+
} else if (input instanceof node_url.URL) {
|
|
6344
|
+
url = input.toString();
|
|
6345
|
+
} else if (input instanceof Request) {
|
|
5717
6346
|
url = input.url;
|
|
5718
6347
|
method = input.method;
|
|
6348
|
+
input.headers.forEach((v, k) => requestHeaders[k] = v);
|
|
5719
6349
|
}
|
|
5720
6350
|
if (init) {
|
|
5721
|
-
if (init.method) method = init.method;
|
|
6351
|
+
if (init.method) method = init.method.toUpperCase();
|
|
5722
6352
|
if (init.headers) {
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
} else if (Array.isArray(init.headers)) {
|
|
5726
|
-
init.headers.forEach(([k, v]) => requestHeaders[k] = v);
|
|
5727
|
-
} else {
|
|
5728
|
-
Object.assign(requestHeaders, init.headers);
|
|
5729
|
-
}
|
|
6353
|
+
const h = new Headers(init.headers);
|
|
6354
|
+
h.forEach((v, k) => requestHeaders[k] = v);
|
|
5730
6355
|
}
|
|
5731
|
-
if (init.body) requestBody = init.body;
|
|
5732
6356
|
}
|
|
5733
6357
|
} catch (e) {
|
|
5734
|
-
console.warn("[FetchInterceptor] Failed to parse request arguments", e);
|
|
5735
6358
|
}
|
|
5736
6359
|
try {
|
|
5737
6360
|
const response = await self.originalFetch.apply(global, [input, init]);
|
|
@@ -5741,14 +6364,11 @@ class FetchInterceptor {
|
|
|
5741
6364
|
method,
|
|
5742
6365
|
url,
|
|
5743
6366
|
requestHeaders,
|
|
5744
|
-
requestBody,
|
|
5745
|
-
status: response.status,
|
|
5746
6367
|
startTime: timestamp,
|
|
5747
6368
|
duration,
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
});
|
|
6369
|
+
status: response.status,
|
|
6370
|
+
...self.extractRequestMeta(url, requestHeaders)
|
|
6371
|
+
}).catch((err) => console.error("[FetchInterceptor] Error processing response:", err));
|
|
5752
6372
|
return response;
|
|
5753
6373
|
} catch (error) {
|
|
5754
6374
|
const duration = performance.now() - startTime;
|
|
@@ -5756,17 +6376,18 @@ class FetchInterceptor {
|
|
|
5756
6376
|
method,
|
|
5757
6377
|
url,
|
|
5758
6378
|
requestHeaders,
|
|
5759
|
-
requestBody,
|
|
5760
6379
|
status: 0,
|
|
5761
6380
|
responseHeaders: {},
|
|
5762
|
-
responseBody: `Network Error: ${String(error)}`,
|
|
5763
6381
|
startTime: timestamp,
|
|
5764
|
-
duration
|
|
6382
|
+
duration,
|
|
6383
|
+
responseBody: `Error: ${error.message}`,
|
|
6384
|
+
...self.extractRequestMeta(url, requestHeaders)
|
|
5765
6385
|
});
|
|
5766
6386
|
throw error;
|
|
5767
6387
|
}
|
|
5768
6388
|
};
|
|
5769
|
-
|
|
6389
|
+
newFetch.__isPatched = true;
|
|
6390
|
+
newFetch.__originalFetch = this.originalFetch;
|
|
5770
6391
|
global.fetch = newFetch;
|
|
5771
6392
|
}
|
|
5772
6393
|
patchNodeRequests() {
|
|
@@ -6111,6 +6732,9 @@ class Dashboard {
|
|
|
6111
6732
|
this.broadcastMetricUpdate(metric);
|
|
6112
6733
|
};
|
|
6113
6734
|
this.metricsCollector = new MetricsCollector(this.db, onCollect);
|
|
6735
|
+
if (app.applicationConfig) {
|
|
6736
|
+
app.applicationConfig.enableMiddlewareTracking = true;
|
|
6737
|
+
}
|
|
6114
6738
|
const fetchInterceptor = new FetchInterceptor();
|
|
6115
6739
|
fetchInterceptor.patch();
|
|
6116
6740
|
fetchInterceptor.on((log) => {
|
|
@@ -6144,6 +6768,10 @@ class Dashboard {
|
|
|
6144
6768
|
responseHeaders: log.responseHeaders
|
|
6145
6769
|
// No handler stack for outbound
|
|
6146
6770
|
};
|
|
6771
|
+
const maxLogs = this.dashboardConfig.maxLogEntries ?? 1e3;
|
|
6772
|
+
if (this.metrics.logs.length >= maxLogs) {
|
|
6773
|
+
this.metrics.logs.shift();
|
|
6774
|
+
}
|
|
6147
6775
|
this.metrics.logs.push(requestData);
|
|
6148
6776
|
const recordId = new surrealdb.RecordId("request", nanoid.nanoid());
|
|
6149
6777
|
const idString = recordId.toString();
|
|
@@ -6171,17 +6799,9 @@ class Dashboard {
|
|
|
6171
6799
|
}
|
|
6172
6800
|
this.mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
6173
6801
|
const hooks = this.getHooks();
|
|
6174
|
-
if (
|
|
6175
|
-
app.
|
|
6802
|
+
if (hooks.onRequestStart) {
|
|
6803
|
+
app.hook("onRequestStart", hooks.onRequestStart);
|
|
6176
6804
|
}
|
|
6177
|
-
const hooksMiddleware = async (ctx, next) => {
|
|
6178
|
-
if (hooks.onRequestStart) {
|
|
6179
|
-
await hooks.onRequestStart(ctx);
|
|
6180
|
-
}
|
|
6181
|
-
ctx._startTime = performance.now();
|
|
6182
|
-
await next();
|
|
6183
|
-
};
|
|
6184
|
-
app.use(hooksMiddleware);
|
|
6185
6805
|
if (hooks.onResponseEnd) {
|
|
6186
6806
|
app.hook("onResponseEnd", hooks.onResponseEnd);
|
|
6187
6807
|
}
|
|
@@ -6428,7 +7048,7 @@ class Dashboard {
|
|
|
6428
7048
|
if (!this.instrumented && app) {
|
|
6429
7049
|
this.instrumentApp(app);
|
|
6430
7050
|
}
|
|
6431
|
-
const registry = app?.
|
|
7051
|
+
const registry = app?.registry;
|
|
6432
7052
|
if (registry) {
|
|
6433
7053
|
this.assignIdsToRegistry(registry, "root");
|
|
6434
7054
|
}
|
|
@@ -6465,23 +7085,48 @@ class Dashboard {
|
|
|
6465
7085
|
});
|
|
6466
7086
|
this.router.post("/replay", async (ctx) => {
|
|
6467
7087
|
const body = await ctx.body();
|
|
6468
|
-
const
|
|
6469
|
-
if (
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
headers
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
7088
|
+
const direction = body.direction || "inbound";
|
|
7089
|
+
if (direction === "outbound") {
|
|
7090
|
+
const start = performance.now();
|
|
7091
|
+
try {
|
|
7092
|
+
const res = await fetch(body.url, {
|
|
7093
|
+
method: body.method,
|
|
7094
|
+
headers: body.headers,
|
|
7095
|
+
body: body.body ? typeof body.body === "object" ? JSON.stringify(body.body) : body.body : void 0
|
|
7096
|
+
});
|
|
7097
|
+
const text = await res.text();
|
|
7098
|
+
const duration = performance.now() - start;
|
|
7099
|
+
const resHeaders = {};
|
|
7100
|
+
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
7101
|
+
return ctx.json({
|
|
7102
|
+
status: res.status,
|
|
7103
|
+
statusText: res.statusText,
|
|
7104
|
+
headers: resHeaders,
|
|
7105
|
+
data: text,
|
|
7106
|
+
duration
|
|
7107
|
+
});
|
|
7108
|
+
} catch (e) {
|
|
7109
|
+
return ctx.json({ error: String(e) }, 500);
|
|
7110
|
+
}
|
|
7111
|
+
} else {
|
|
7112
|
+
const app = this[$appRoot];
|
|
7113
|
+
if (!app) return unknownError(ctx);
|
|
7114
|
+
try {
|
|
7115
|
+
const result = await app.internalRequest({
|
|
7116
|
+
method: body.method,
|
|
7117
|
+
path: body.url,
|
|
7118
|
+
// or path
|
|
7119
|
+
headers: body.headers,
|
|
7120
|
+
body: body.body
|
|
7121
|
+
});
|
|
7122
|
+
return ctx.json({
|
|
7123
|
+
status: result.status,
|
|
7124
|
+
headers: result.headers,
|
|
7125
|
+
data: result.body
|
|
7126
|
+
});
|
|
7127
|
+
} catch (e) {
|
|
7128
|
+
return ctx.json({ error: String(e) }, 500);
|
|
7129
|
+
}
|
|
6485
7130
|
}
|
|
6486
7131
|
});
|
|
6487
7132
|
this.router.get("/**", async (ctx) => {
|
|
@@ -6519,6 +7164,14 @@ class Dashboard {
|
|
|
6519
7164
|
const linkPattern = this.getLinkPattern();
|
|
6520
7165
|
const integrations = this.detectIntegrations();
|
|
6521
7166
|
const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
|
|
7167
|
+
const ignorePaths = [
|
|
7168
|
+
...this.dashboardConfig.ignorePaths || [],
|
|
7169
|
+
// Add default ignores for integrations
|
|
7170
|
+
...Object.values(integrations).filter((p) => !!p).flatMap((p) => {
|
|
7171
|
+
const clean = p.endsWith("/") ? p.slice(0, -1) : p;
|
|
7172
|
+
return [clean, `${clean}/**`];
|
|
7173
|
+
})
|
|
7174
|
+
];
|
|
6522
7175
|
const html = renderToString(DashboardApp({
|
|
6523
7176
|
metrics: this.metrics,
|
|
6524
7177
|
uptime,
|
|
@@ -6526,7 +7179,8 @@ class Dashboard {
|
|
|
6526
7179
|
linkPattern,
|
|
6527
7180
|
integrations,
|
|
6528
7181
|
base: mountPath,
|
|
6529
|
-
getRequestHeadersSource
|
|
7182
|
+
getRequestHeadersSource,
|
|
7183
|
+
ignorePaths
|
|
6530
7184
|
}));
|
|
6531
7185
|
return ctx.html(`<!DOCTYPE html>${html}`);
|
|
6532
7186
|
});
|
|
@@ -6673,12 +7327,15 @@ class Dashboard {
|
|
|
6673
7327
|
getHooks() {
|
|
6674
7328
|
return {
|
|
6675
7329
|
onRequestStart: (ctx) => {
|
|
7330
|
+
if (ctx.path.startsWith(this.mountPath)) return;
|
|
6676
7331
|
const app = this[$appRoot];
|
|
6677
7332
|
if (!this.instrumented && app) {
|
|
6678
7333
|
this.instrumentApp(app);
|
|
6679
7334
|
}
|
|
6680
7335
|
this.metrics.totalRequests++;
|
|
6681
7336
|
this.metrics.activeRequests++;
|
|
7337
|
+
ctx._startTime = performance.now();
|
|
7338
|
+
ctx._reqStartTime = Date.now();
|
|
6682
7339
|
ctx[$debug] = new Collector(this);
|
|
6683
7340
|
if (!this.broadcastTimer) {
|
|
6684
7341
|
this.broadcastTimer = setTimeout(() => {
|
|
@@ -6768,7 +7425,7 @@ class Dashboard {
|
|
|
6768
7425
|
url: ctx.url.toString(),
|
|
6769
7426
|
status: response.status,
|
|
6770
7427
|
duration,
|
|
6771
|
-
timestamp: Date.now(),
|
|
7428
|
+
timestamp: ctx._reqStartTime || Date.now() - duration,
|
|
6772
7429
|
handlerStack: this.serializeHandlerStack(ctx.handlerStack),
|
|
6773
7430
|
body: this.serializeBody(ctx.responseBody),
|
|
6774
7431
|
requestBody: ctx.bodyData || ctx.requestBody,
|
|
@@ -6789,17 +7446,12 @@ class Dashboard {
|
|
|
6789
7446
|
responseHeaders: resHeaders
|
|
6790
7447
|
};
|
|
6791
7448
|
this.metrics.logs.push(logEntry);
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
...logEntry,
|
|
6797
|
-
direction: "inbound"
|
|
6798
|
-
}
|
|
6799
|
-
});
|
|
6800
|
-
} catch (e) {
|
|
7449
|
+
this.db.create(new surrealdb.RecordId("request", ctx.requestId), {
|
|
7450
|
+
...logEntry,
|
|
7451
|
+
direction: "inbound"
|
|
7452
|
+
}).catch((e) => {
|
|
6801
7453
|
console.error("Failed to record request log", e);
|
|
6802
|
-
}
|
|
7454
|
+
});
|
|
6803
7455
|
const retention = this.dashboardConfig.retentionMs ?? 72e5;
|
|
6804
7456
|
const cutoff = Date.now() - retention;
|
|
6805
7457
|
if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
|
|
@@ -7159,10 +7811,77 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
7159
7811
|
}
|
|
7160
7812
|
}
|
|
7161
7813
|
}
|
|
7814
|
+
function createLimitStream(maxSize) {
|
|
7815
|
+
let size = 0;
|
|
7816
|
+
return new TransformStream({
|
|
7817
|
+
transform(chunk, controller) {
|
|
7818
|
+
size += chunk.byteLength || chunk.length;
|
|
7819
|
+
if (size > maxSize) {
|
|
7820
|
+
controller.error(new Error(`Decompressed body size exceeded limit of ${maxSize} bytes`));
|
|
7821
|
+
} else {
|
|
7822
|
+
controller.enqueue(chunk);
|
|
7823
|
+
}
|
|
7824
|
+
}
|
|
7825
|
+
});
|
|
7826
|
+
}
|
|
7162
7827
|
function Compression(options = {}) {
|
|
7163
7828
|
const threshold = options.threshold ?? 512;
|
|
7164
7829
|
const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
|
|
7830
|
+
const decompress = options.decompress ?? true;
|
|
7831
|
+
const maxDecompressedSize = options.maxDecompressedSize ?? 10 * 1024 * 1024;
|
|
7165
7832
|
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
7833
|
+
const requestEncoding = ctx.headers.get("content-encoding");
|
|
7834
|
+
if (decompress && requestEncoding && !ctx.headers.get("content-encoding")?.includes("identity") && ctx.req.body) {
|
|
7835
|
+
let stream = null;
|
|
7836
|
+
if (requestEncoding.includes("br")) {
|
|
7837
|
+
const decompressor = zlib__namespace.createBrotliDecompress();
|
|
7838
|
+
const nodeStream = node_stream.Readable.fromWeb(ctx.req.body);
|
|
7839
|
+
stream = node_stream.Readable.toWeb(nodeStream.pipe(decompressor));
|
|
7840
|
+
} else if (requestEncoding.includes("gzip")) {
|
|
7841
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
7842
|
+
stream = ctx.req.body.pipeThrough(new DecompressionStream("gzip"));
|
|
7843
|
+
} else {
|
|
7844
|
+
const decompressor = zlib__namespace.createGunzip();
|
|
7845
|
+
const nodeStream = node_stream.Readable.fromWeb(ctx.req.body);
|
|
7846
|
+
stream = node_stream.Readable.toWeb(nodeStream.pipe(decompressor));
|
|
7847
|
+
}
|
|
7848
|
+
} else if (requestEncoding.includes("deflate")) {
|
|
7849
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
7850
|
+
stream = ctx.req.body.pipeThrough(new DecompressionStream("deflate"));
|
|
7851
|
+
} else {
|
|
7852
|
+
const decompressor = zlib__namespace.createInflate();
|
|
7853
|
+
const nodeStream = node_stream.Readable.fromWeb(ctx.req.body);
|
|
7854
|
+
stream = node_stream.Readable.toWeb(nodeStream.pipe(decompressor));
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
if (stream) {
|
|
7858
|
+
const outputStream = stream.pipeThrough(createLimitStream(maxDecompressedSize));
|
|
7859
|
+
const originalIp = ctx.ip;
|
|
7860
|
+
const originalReq = ctx.req;
|
|
7861
|
+
const newHeaders = new Headers(originalReq.headers);
|
|
7862
|
+
newHeaders.delete("content-encoding");
|
|
7863
|
+
newHeaders.delete("content-length");
|
|
7864
|
+
const newReq = new Proxy(originalReq, {
|
|
7865
|
+
get(target, prop, receiver) {
|
|
7866
|
+
if (prop === "body") return outputStream;
|
|
7867
|
+
if (prop === "headers") return newHeaders;
|
|
7868
|
+
if (prop === "json") return async () => JSON.parse(await new Response(outputStream).text());
|
|
7869
|
+
if (prop === "text") return async () => await new Response(outputStream).text();
|
|
7870
|
+
if (prop === "arrayBuffer") return async () => await new Response(outputStream).arrayBuffer();
|
|
7871
|
+
if (prop === "blob") return async () => await new Response(outputStream).blob();
|
|
7872
|
+
if (prop === "formData") return async () => await new Response(outputStream).formData();
|
|
7873
|
+
return Reflect.get(target, prop, target);
|
|
7874
|
+
}
|
|
7875
|
+
});
|
|
7876
|
+
ctx.request = newReq;
|
|
7877
|
+
if (originalIp) {
|
|
7878
|
+
Object.defineProperty(ctx, "ip", {
|
|
7879
|
+
configurable: true,
|
|
7880
|
+
get: () => originalIp
|
|
7881
|
+
});
|
|
7882
|
+
}
|
|
7883
|
+
}
|
|
7884
|
+
}
|
|
7166
7885
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
7167
7886
|
let method = null;
|
|
7168
7887
|
if (acceptEncoding.includes("br")) method = "br";
|
|
@@ -7614,7 +8333,7 @@ function openApiValidator() {
|
|
|
7614
8333
|
if (validators.body) {
|
|
7615
8334
|
let body;
|
|
7616
8335
|
try {
|
|
7617
|
-
body = await ctx.
|
|
8336
|
+
body = await ctx.body();
|
|
7618
8337
|
} catch {
|
|
7619
8338
|
body = {};
|
|
7620
8339
|
}
|
|
@@ -7736,8 +8455,7 @@ function enableOpenApiValidation(app) {
|
|
|
7736
8455
|
}
|
|
7737
8456
|
function SecurityHeaders(options = {}) {
|
|
7738
8457
|
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
7739
|
-
const
|
|
7740
|
-
const set = (k, v) => headers[k] = v;
|
|
8458
|
+
const set = (k, v) => ctx.response.set(k, v);
|
|
7741
8459
|
if (options.dnsPrefetchControl !== false) {
|
|
7742
8460
|
const allow = options.dnsPrefetchControl?.allow;
|
|
7743
8461
|
set("X-DNS-Prefetch-Control", allow ? "on" : "off");
|
|
@@ -7783,14 +8501,6 @@ function SecurityHeaders(options = {}) {
|
|
|
7783
8501
|
}
|
|
7784
8502
|
if (options.hidePoweredBy !== false) ;
|
|
7785
8503
|
const response = await next();
|
|
7786
|
-
if (response instanceof Response) {
|
|
7787
|
-
const headerEntries = Object.entries(headers);
|
|
7788
|
-
for (let i = 0; i < headerEntries.length; i++) {
|
|
7789
|
-
const [k, v] = headerEntries[i];
|
|
7790
|
-
response.headers.set(k, v);
|
|
7791
|
-
}
|
|
7792
|
-
return response;
|
|
7793
|
-
}
|
|
7794
8504
|
return response;
|
|
7795
8505
|
};
|
|
7796
8506
|
securityHeadersMiddleware.isBuiltin = true;
|
|
@@ -8066,6 +8776,7 @@ exports.$bodyParseError = $bodyParseError;
|
|
|
8066
8776
|
exports.$bodyParsed = $bodyParsed;
|
|
8067
8777
|
exports.$bodyType = $bodyType;
|
|
8068
8778
|
exports.$cachedBody = $cachedBody;
|
|
8779
|
+
exports.$cachedCookies = $cachedCookies;
|
|
8069
8780
|
exports.$cachedHost = $cachedHost;
|
|
8070
8781
|
exports.$cachedHostname = $cachedHostname;
|
|
8071
8782
|
exports.$cachedOrigin = $cachedOrigin;
|