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