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.
Files changed (48) hide show
  1. package/README.md +46 -1815
  2. package/dist/{analyzer-CnKnQ5KV.js → analyzer-BkNQHWj4.js} +2 -2
  3. package/dist/{analyzer-CnKnQ5KV.js.map → analyzer-BkNQHWj4.js.map} +1 -1
  4. package/dist/{analyzer-BAhvpNY_.cjs → analyzer-DM-OlRq8.cjs} +2 -2
  5. package/dist/{analyzer-BAhvpNY_.cjs.map → analyzer-DM-OlRq8.cjs.map} +1 -1
  6. package/dist/{analyzer.impl-CfpMu4-g.cjs → analyzer.impl-CVJ8zfGQ.cjs} +11 -3
  7. package/dist/analyzer.impl-CVJ8zfGQ.cjs.map +1 -0
  8. package/dist/{analyzer.impl-DCiqlXI5.js → analyzer.impl-CsA1bS_s.js} +11 -3
  9. package/dist/analyzer.impl-CsA1bS_s.js.map +1 -0
  10. package/dist/cli.cjs +1 -1
  11. package/dist/cli.js +1 -1
  12. package/dist/context.d.ts +40 -8
  13. package/dist/index.cjs +1011 -300
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.js +1011 -300
  16. package/dist/index.js.map +1 -1
  17. package/dist/plugins/application/api-explorer/static/theme.css +4 -0
  18. package/dist/plugins/application/auth.d.ts +5 -0
  19. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +12 -0
  20. package/dist/plugins/application/dashboard/plugin.d.ts +9 -0
  21. package/dist/plugins/application/dashboard/static/requests.js +537 -251
  22. package/dist/plugins/application/dashboard/static/tabulator.css +23 -3
  23. package/dist/plugins/application/dashboard/static/theme.css +4 -0
  24. package/dist/plugins/application/mcp-server/plugin.d.ts +39 -0
  25. package/dist/plugins/application/openapi/analyzer.impl.d.ts +4 -0
  26. package/dist/plugins/middleware/compression.d.ts +12 -2
  27. package/dist/plugins/middleware/rate-limit.d.ts +5 -0
  28. package/dist/router.d.ts +6 -5
  29. package/dist/server.d.ts +22 -0
  30. package/dist/shokupan.d.ts +24 -1
  31. package/dist/util/adapter/bun.d.ts +8 -0
  32. package/dist/util/adapter/index.d.ts +4 -0
  33. package/dist/util/adapter/interface.d.ts +12 -0
  34. package/dist/util/adapter/node.d.ts +8 -0
  35. package/dist/util/adapter/wintercg.d.ts +5 -0
  36. package/dist/util/body-parser.d.ts +30 -0
  37. package/dist/util/decorators.d.ts +20 -3
  38. package/dist/util/di.d.ts +3 -8
  39. package/dist/util/metadata.d.ts +18 -0
  40. package/dist/util/request.d.ts +1 -0
  41. package/dist/util/symbol.d.ts +1 -0
  42. package/dist/util/types.d.ts +132 -3
  43. package/package.json +3 -1
  44. package/dist/analyzer.impl-CfpMu4-g.cjs.map +0 -1
  45. package/dist/analyzer.impl-DCiqlXI5.js.map +0 -1
  46. package/dist/plugins/application/dashboard/static/failures.js +0 -85
  47. package/dist/plugins/application/http-server.d.ts +0 -13
  48. 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-BAhvpNY_.cjs");
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 (Array.isArray(q[key])) {
422
- q[key].push(value);
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
- q[key] = [q[key], value];
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 contentType = this.request.headers.get("content-type") || "";
619
- if (contentType.includes("application/json") || contentType.includes("+json")) {
620
- const parserType = this.app?.applicationConfig?.jsonParser || "native";
621
- if (parserType === "native") {
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
- this[$bodyParseError] = error;
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
- finalHeaders.set("Location", url instanceof Promise ? await url : url);
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-BAhvpNY_.cjs"));
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 fileBuffer = await promises$1.readFile(finalPath, { encoding: "binary" });
1701
- response = new Response(fileBuffer);
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
- const instance = new target();
1729
- this.services.set(target, instance);
1730
- return instance;
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
- function Injectable() {
1734
- return (target) => {
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.registerControllerInstance(instance);
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
- getComponentRegistry() {
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.getComponentRegistry()
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
- registerControllerInstance(controller) {
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
- let factory = app.applicationConfig.serverFactory;
3250
- if (!factory) {
3251
- factory = createHttpServer();
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
- const serveOptions = {
3254
- port,
3255
- hostname: app.applicationConfig.hostname,
3256
- development: app.applicationConfig.development,
3257
- fetch: app.fetch.bind(app),
3258
- reusePort: app.applicationConfig.reusePort
3259
- // Node adapter might not support all options exactly the same
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
- this.server = await factory(serveOptions);
3262
- return this.server;
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
- async listen(port) {
3572
- const finalPort = port ?? this.applicationConfig.port ?? 3e3;
3573
- if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
3574
- throw new Error("Invalid port number");
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}:${finalPort}/.well-known/openapi.yaml`,
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}:${finalPort}/logo.png`,
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}:${finalPort}/legal`
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}:${finalPort}/`,
3626
- spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`
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
- let adapter = this.applicationConfig.adapter;
3668
- if (!adapter) {
3669
- if (typeof Bun !== "undefined") {
3670
- this.applicationConfig.adapter = "bun";
3671
- adapter = new BunAdapter();
3672
- } else {
3673
- this.applicationConfig.adapter = "node";
3674
- adapter = new NodeAdapter();
3675
- }
3676
- } else if (adapter === "bun") {
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.server) {
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
- response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
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 body = { error: err.message || "Internal Server Error" };
3866
- if (err.errors) body.errors = err.errors;
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
- }, windowMs);
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 Use(...middleware) {
3991
- return (target, propertyKey, descriptor) => {
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: "style.css" }),
4258
- /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "theme.css" }),
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: "explorer-client.mjs", type: "module" })
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(file, "utf-8");
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 element = ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec });
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 scroller", id: "sidebar", children: [
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-BAhvpNY_.cjs"));
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(file, "utf8");
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: calc(100vh - 170px);", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "margin: 0 2rem; display: flex; gap: 1rem; height: 100%;", children: [
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-y: auto; flex-shrink: 0; background: var(--bg-secondary); border: 1px solid var(--card-border); position: relative;", children: [
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.jsxs("div", { style: "padding: 1rem;", children: [
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
- this.originalFetch = global.fetch;
5688
- this.originalHttpRequest = http.request;
5689
- this.originalHttpsRequest = https.request;
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 instanceof node_url.URL) {
5713
- url = input.toString();
5714
- } else if (typeof input === "string") {
6341
+ if (typeof input === "string") {
5715
6342
  url = input;
5716
- } else if (typeof input === "object" && "url" in input) {
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
- if (init.headers instanceof Headers) {
5724
- init.headers.forEach((v, k) => requestHeaders[k] = v);
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
- ...self.extractRequestMeta(url, requestHeaders),
5749
- protocol: "1.1"
5750
- // native fetch doesn't expose this easily, assume 1.1/2
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
- Object.assign(newFetch, this.originalFetch);
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 (!app.middleware) {
6175
- app.middleware = [];
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?.getComponentRegistry?.();
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 app = this[$appRoot];
6469
- if (!app) return unknownError(ctx);
6470
- try {
6471
- const result = await app.processRequest({
6472
- method: body.method,
6473
- path: body.url,
6474
- // or path
6475
- headers: body.headers,
6476
- body: body.body
6477
- });
6478
- return ctx.json({
6479
- status: result.status,
6480
- headers: result.headers,
6481
- data: result.data
6482
- });
6483
- } catch (e) {
6484
- return ctx.json({ error: String(e) }, 500);
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
- try {
6793
- await this.db.query("UPSERT $id CONTENT $data", {
6794
- id: new surrealdb.RecordId("request", ctx.requestId),
6795
- data: {
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.req.json().catch(() => ({}));
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 headers = {};
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;