shokupan 0.11.0 → 0.13.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 (65) hide show
  1. package/README.md +47 -1815
  2. package/dist/{analyzer-CnKnQ5KV.js → analyzer-B0fMzeIo.js} +2 -2
  3. package/dist/{analyzer-CnKnQ5KV.js.map → analyzer-B0fMzeIo.js.map} +1 -1
  4. package/dist/{analyzer-BAhvpNY_.cjs → analyzer-BOtveWL-.cjs} +2 -2
  5. package/dist/{analyzer-BAhvpNY_.cjs.map → analyzer-BOtveWL-.cjs.map} +1 -1
  6. package/dist/{analyzer.impl-CfpMu4-g.cjs → analyzer.impl-CUDO6vpn.cjs} +82 -7
  7. package/dist/analyzer.impl-CUDO6vpn.cjs.map +1 -0
  8. package/dist/{analyzer.impl-DCiqlXI5.js → analyzer.impl-DmHe92Oi.js} +82 -7
  9. package/dist/analyzer.impl-DmHe92Oi.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 +2876 -506
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.js +2911 -541
  17. package/dist/index.js.map +1 -1
  18. package/dist/plugins/application/api-explorer/static/theme.css +4 -0
  19. package/dist/plugins/application/auth.d.ts +5 -0
  20. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +12 -0
  21. package/dist/plugins/application/dashboard/plugin.d.ts +9 -0
  22. package/dist/plugins/application/dashboard/static/requests.js +537 -251
  23. package/dist/plugins/application/dashboard/static/tabulator.css +23 -3
  24. package/dist/plugins/application/dashboard/static/theme.css +4 -0
  25. package/dist/plugins/application/error-view/index.d.ts +14 -0
  26. package/dist/plugins/application/error-view/monkeypatch.d.ts +9 -0
  27. package/dist/plugins/application/error-view/util/source-reader.d.ts +10 -0
  28. package/dist/plugins/application/error-view/views/error.d.ts +2 -0
  29. package/dist/plugins/application/error-view/views/status.d.ts +2 -0
  30. package/dist/plugins/application/htmx/index.d.ts +39 -0
  31. package/dist/plugins/application/mcp-server/plugin.d.ts +38 -0
  32. package/dist/plugins/application/openapi/analyzer.impl.d.ts +4 -0
  33. package/dist/plugins/application/openapi/test-setup.d.ts +1 -0
  34. package/dist/plugins/application/opentelemetry/index.d.ts +33 -0
  35. package/dist/plugins/middleware/compression.d.ts +12 -2
  36. package/dist/plugins/middleware/rate-limit.d.ts +5 -0
  37. package/dist/plugins/middleware/session.d.ts +4 -4
  38. package/dist/plugins/resilience/decorators.d.ts +23 -0
  39. package/dist/plugins/resilience/factory.d.ts +5 -0
  40. package/dist/plugins/resilience/index.d.ts +2 -0
  41. package/dist/router.d.ts +25 -9
  42. package/dist/server.d.ts +22 -0
  43. package/dist/shokupan.d.ts +24 -1
  44. package/dist/util/adapter/bun.d.ts +8 -0
  45. package/dist/util/adapter/index.d.ts +4 -0
  46. package/dist/util/adapter/interface.d.ts +12 -0
  47. package/dist/util/adapter/node.d.ts +8 -0
  48. package/dist/util/adapter/wintercg.d.ts +5 -0
  49. package/dist/util/body-parser.d.ts +30 -0
  50. package/dist/util/decorators.d.ts +58 -3
  51. package/dist/util/di.d.ts +3 -8
  52. package/dist/util/env-loader.d.ts +99 -0
  53. package/dist/util/mcp-protocol.d.ts +52 -0
  54. package/dist/util/metadata.d.ts +18 -0
  55. package/dist/util/promise.d.ts +16 -0
  56. package/dist/util/request.d.ts +1 -0
  57. package/dist/util/symbol.d.ts +5 -0
  58. package/dist/util/types.d.ts +140 -3
  59. package/package.json +37 -10
  60. package/dist/analyzer.impl-CfpMu4-g.cjs.map +0 -1
  61. package/dist/analyzer.impl-DCiqlXI5.js.map +0 -1
  62. package/dist/plugins/application/dashboard/static/failures.js +0 -85
  63. package/dist/plugins/application/http-server.d.ts +0 -13
  64. package/dist/util/adapter/adapters.d.ts +0 -19
  65. package/dist/util/instrumentation.d.ts +0 -9
package/dist/index.cjs CHANGED
@@ -25,12 +25,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
25
25
  const nanoid = require("nanoid");
26
26
  const promises = require("node:fs/promises");
27
27
  const node_util = require("node:util");
28
+ const surrealdb = require("surrealdb");
28
29
  const eta$1 = require("eta");
29
30
  const promises$1 = require("fs/promises");
30
31
  const path = require("path");
31
- const api = require("@opentelemetry/api");
32
- const surrealdb = require("surrealdb");
33
- const jsYaml = require("js-yaml");
32
+ const cockatiel = require("cockatiel");
34
33
  const http$1 = require("node:http");
35
34
  require("node:https");
36
35
  const node_async_hooks = require("node:async_hooks");
@@ -43,8 +42,11 @@ const net = require("node:net");
43
42
  const os = require("node:os");
44
43
  const node_module = require("node:module");
45
44
  const node_perf_hooks = require("node:perf_hooks");
45
+ const bun = require("bun");
46
+ const analyzer_impl = require("./analyzer.impl-CUDO6vpn.cjs");
46
47
  const fs$1 = require("node:fs");
47
- const analyzer = require("./analyzer-BAhvpNY_.cjs");
48
+ const analyzer = require("./analyzer-BOtveWL-.cjs");
49
+ const node_stream = require("node:stream");
48
50
  const zlib = require("node:zlib");
49
51
  const Ajv = require("ajv");
50
52
  const addFormats = require("ajv-formats");
@@ -70,6 +72,114 @@ function _interopNamespaceDefault(e) {
70
72
  const http__namespace = /* @__PURE__ */ _interopNamespaceDefault(http$1);
71
73
  const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
72
74
  const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
75
+ class BodyParser {
76
+ /**
77
+ * Parses the body of a request based on Content-Type header.
78
+ * @param req The ShokupanRequest object
79
+ * @param config Application configuration for limits and parser options
80
+ * @returns The parsed body or throws an error
81
+ */
82
+ static async parse(req, config = {}) {
83
+ const contentType = req.headers.get("content-type") || "";
84
+ const maxBodySize = config.maxBodySize ?? 10 * 1024 * 1024;
85
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
86
+ return {
87
+ type: "json",
88
+ body: await BodyParser.parseJson(req, config.jsonParser || "native", maxBodySize)
89
+ };
90
+ } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
91
+ return {
92
+ type: "formData",
93
+ body: await BodyParser.parseFormData(req, maxBodySize)
94
+ };
95
+ } else {
96
+ return {
97
+ type: "text",
98
+ body: await BodyParser.readRawBody(req, maxBodySize)
99
+ };
100
+ }
101
+ }
102
+ /**
103
+ * Parsing helper for JSON
104
+ */
105
+ static async parseJson(req, parserType, maxBodySize) {
106
+ const rawText = await BodyParser.readRawBody(req, maxBodySize);
107
+ if (parserType === "native") {
108
+ if (!rawText) return {};
109
+ return JSON.parse(rawText);
110
+ } else {
111
+ const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
112
+ const parser = getJSONParser(parserType);
113
+ return parser(rawText);
114
+ }
115
+ }
116
+ /**
117
+ * Parsing helper for FormData
118
+ */
119
+ static async parseFormData(req, maxBodySize) {
120
+ const clHeader = req.headers.get("content-length");
121
+ if (!clHeader) {
122
+ const err = new Error("Length Required");
123
+ err.status = 411;
124
+ throw err;
125
+ }
126
+ const cl = parseInt(clHeader, 10);
127
+ if (isNaN(cl)) {
128
+ const err = new Error("Bad Request");
129
+ err.status = 400;
130
+ throw err;
131
+ }
132
+ if (cl > maxBodySize) {
133
+ const err = new Error("Payload Too Large");
134
+ err.status = 413;
135
+ throw err;
136
+ }
137
+ return req.formData();
138
+ }
139
+ /**
140
+ * Reads raw body as string with size enforcement
141
+ */
142
+ static async readRawBody(req, maxBodySize) {
143
+ if (typeof req.body === "string") {
144
+ const body = req.body;
145
+ if (body.length > maxBodySize) {
146
+ const err = new Error("Payload Too Large");
147
+ err.status = 413;
148
+ throw err;
149
+ }
150
+ return body;
151
+ }
152
+ const reader = req.body?.getReader();
153
+ if (!reader) {
154
+ return "";
155
+ }
156
+ const chunks = [];
157
+ let totalSize = 0;
158
+ try {
159
+ while (true) {
160
+ const { done, value } = await reader.read();
161
+ if (done) break;
162
+ totalSize += value.length;
163
+ if (totalSize > maxBodySize) {
164
+ const err = new Error("Payload Too Large");
165
+ err.status = 413;
166
+ throw err;
167
+ }
168
+ chunks.push(value);
169
+ }
170
+ } finally {
171
+ reader.releaseLock();
172
+ }
173
+ const result = new Uint8Array(totalSize);
174
+ let offset = 0;
175
+ for (let i = 0; i < chunks.length; i++) {
176
+ const chunk = chunks[i];
177
+ result.set(chunk, offset);
178
+ offset += chunk.length;
179
+ }
180
+ return new TextDecoder().decode(result);
181
+ }
182
+ }
73
183
  const HTTP_STATUS = {
74
184
  // 2xx Success
75
185
  OK: 200,
@@ -260,9 +370,14 @@ const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol"
260
370
  const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
261
371
  const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
262
372
  const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
373
+ const $cachedCookies = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedCookies");
263
374
  const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
264
375
  const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
265
376
  const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
377
+ const $mcpTools = /* @__PURE__ */ Symbol.for("Shokupan.mcp.tools");
378
+ const $mcpPrompts = /* @__PURE__ */ Symbol.for("Shokupan.mcp.prompts");
379
+ const $mcpResources = /* @__PURE__ */ Symbol.for("Shokupan.mcp.resources");
380
+ const $resilienceConfig = /* @__PURE__ */ Symbol.for("Shokupan.resilience.config");
266
381
  function isValidCookieDomain(domain, currentHost) {
267
382
  const hostWithoutPort = currentHost.split(":")[0];
268
383
  if (domain === hostWithoutPort) return true;
@@ -318,6 +433,7 @@ class ShokupanContext {
318
433
  [$cachedHost];
319
434
  [$cachedOrigin];
320
435
  [$cachedQuery];
436
+ [$cachedCookies];
321
437
  disconnectCallbacks = [];
322
438
  /**
323
439
  * Registers a callback to be executed when the associated WebSocket disconnects.
@@ -415,13 +531,20 @@ class ShokupanContext {
415
531
  if (this[$cachedQuery]) return this[$cachedQuery];
416
532
  const q = /* @__PURE__ */ Object.create(null);
417
533
  const blocklist = ["__proto__", "constructor", "prototype"];
534
+ const mode = this.app?.applicationConfig?.queryParserMode || "extended";
418
535
  this.url.searchParams.forEach((value, key) => {
419
536
  if (blocklist.includes(key)) return;
420
537
  if (Object.prototype.hasOwnProperty.call(q, key)) {
421
- if (Array.isArray(q[key])) {
422
- q[key].push(value);
538
+ if (mode === "strict") {
539
+ throw new Error(`Duplicate query parameter '${key}' is not allowed in strict mode.`);
540
+ } else if (mode === "simple") {
541
+ q[key] = value;
423
542
  } else {
424
- q[key] = [q[key], value];
543
+ if (Array.isArray(q[key])) {
544
+ q[key].push(value);
545
+ } else {
546
+ q[key] = [q[key], value];
547
+ }
425
548
  }
426
549
  } else {
427
550
  q[key] = value;
@@ -430,6 +553,28 @@ class ShokupanContext {
430
553
  this[$cachedQuery] = q;
431
554
  return q;
432
555
  }
556
+ /**
557
+ * Request cookies
558
+ */
559
+ get cookies() {
560
+ if (this[$cachedCookies]) return this[$cachedCookies];
561
+ const c = /* @__PURE__ */ Object.create(null);
562
+ const cookieHeader = this.request.headers.get("cookie");
563
+ if (cookieHeader) {
564
+ const pairs = cookieHeader.split(";");
565
+ for (let i = 0; i < pairs.length; i++) {
566
+ const pair = pairs[i];
567
+ const index = pair.indexOf("=");
568
+ if (index > 0) {
569
+ const key = pair.slice(0, index).trim();
570
+ const value = pair.slice(index + 1).trim();
571
+ c[key] = decodeURIComponent(value);
572
+ }
573
+ }
574
+ }
575
+ this[$cachedCookies] = c;
576
+ return c;
577
+ }
433
578
  /**
434
579
  * Client IP address
435
580
  */
@@ -604,6 +749,10 @@ class ShokupanContext {
604
749
  }
605
750
  return h;
606
751
  }
752
+ /**
753
+ * Read request body with caching to avoid double parsing.
754
+ * The body is only parsed once and cached for subsequent reads.
755
+ */
607
756
  /**
608
757
  * Read request body with caching to avoid double parsing.
609
758
  * The body is only parsed once and cached for subsequent reads.
@@ -615,29 +764,10 @@ class ShokupanContext {
615
764
  if (this[$bodyParsed] === true) {
616
765
  return this[$cachedBody];
617
766
  }
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
- }
767
+ const config = this.app?.applicationConfig || {};
768
+ const { type, body } = await BodyParser.parse(this.request, config);
769
+ this[$bodyType] = type;
770
+ this[$cachedBody] = body;
641
771
  this[$bodyParsed] = true;
642
772
  return this[$cachedBody];
643
773
  }
@@ -653,45 +783,22 @@ class ShokupanContext {
653
783
  if (this.request.method === "GET" || this.request.method === "HEAD") {
654
784
  return;
655
785
  }
786
+ const maxBodySize = this.app?.applicationConfig?.maxBodySize ?? 10 * 1024 * 1024;
787
+ const contentLength = parseInt(this.request.headers.get("content-length") || "0", 10);
788
+ if (contentLength > maxBodySize) {
789
+ this[$bodyParseError] = new Error("Payload Too Large");
790
+ this[$bodyParseError].status = 413;
791
+ return;
792
+ }
656
793
  try {
657
794
  await this.body();
658
795
  } 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;
796
+ if (error.status === 413 || error.message === "Payload Too Large") {
797
+ this[$bodyParseError] = error;
798
+ } else {
799
+ this[$bodyParseError] = error;
683
800
  }
684
- } finally {
685
- reader.releaseLock();
686
801
  }
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
802
  }
696
803
  /**
697
804
  * Send a response
@@ -791,7 +898,15 @@ class ShokupanContext {
791
898
  }
792
899
  this.response.status = status;
793
900
  const finalHeaders = this.mergeHeaders();
794
- finalHeaders.set("Location", url instanceof Promise ? await url : url);
901
+ const targetUrl = url instanceof Promise ? await url : url;
902
+ if (targetUrl.startsWith("//")) {
903
+ throw new Error("Invalid redirect: Protocol-relative URLs are not allowed.");
904
+ }
905
+ const lowerUrl = targetUrl.toLowerCase();
906
+ if (lowerUrl.startsWith("javascript:") || lowerUrl.startsWith("data:") || lowerUrl.startsWith("vbscript:")) {
907
+ throw new Error(`Invalid redirect: Unsafe protocol '${targetUrl.split(":")[0]}'`);
908
+ }
909
+ finalHeaders.set("Location", targetUrl);
795
910
  this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
796
911
  return this[$finalResponse];
797
912
  }
@@ -849,6 +964,185 @@ class ShokupanContext {
849
964
  const html = await this.renderer(element, args);
850
965
  return this.html(html, status, headers);
851
966
  }
967
+ /**
968
+ * Pipe a ReadableStream to the response
969
+ * @param stream ReadableStream to pipe
970
+ * @param options Response options (status, headers)
971
+ */
972
+ pipe(stream, options) {
973
+ const headers = this.mergeHeaders(options?.headers);
974
+ const status = options?.status ?? this.response.status ?? 200;
975
+ if (this.app?.applicationConfig?.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
976
+ throw new Error(`Invalid HTTP status code: ${status}`);
977
+ }
978
+ this[$finalResponse] = new Response(stream, { status, headers });
979
+ return this[$finalResponse];
980
+ }
981
+ /**
982
+ * Internal helper to create a streaming response with common infrastructure
983
+ * @private
984
+ */
985
+ createStreamHelper(helperFactory, callback, onError, headers) {
986
+ let controller;
987
+ const aborted = { value: false };
988
+ const abortCallbacks = [];
989
+ const encoder = new TextEncoder();
990
+ let helper;
991
+ const stream = new ReadableStream({
992
+ start(ctrl) {
993
+ controller = ctrl;
994
+ helper = helperFactory(controller, aborted, abortCallbacks, encoder);
995
+ (async () => {
996
+ try {
997
+ await callback(helper);
998
+ controller.close();
999
+ } catch (err) {
1000
+ if (onError) {
1001
+ try {
1002
+ await onError(err, helper);
1003
+ } catch (handlerErr) {
1004
+ console.error("Error in stream error handler:", handlerErr);
1005
+ }
1006
+ } else {
1007
+ console.error("Stream error:", err);
1008
+ }
1009
+ if (!aborted.value) {
1010
+ controller.close();
1011
+ }
1012
+ }
1013
+ })();
1014
+ },
1015
+ async pull() {
1016
+ },
1017
+ cancel() {
1018
+ aborted.value = true;
1019
+ abortCallbacks.forEach((cb) => {
1020
+ try {
1021
+ cb();
1022
+ } catch (err) {
1023
+ console.error("Error in abort callback:", err);
1024
+ }
1025
+ });
1026
+ }
1027
+ });
1028
+ return this.pipe(stream, { headers });
1029
+ }
1030
+ /**
1031
+ * Generic streaming helper for binary/text data
1032
+ * @param callback Callback function that receives a StreamHelper
1033
+ * @param onError Optional error handler
1034
+ */
1035
+ stream(callback, onError) {
1036
+ return this.createStreamHelper(
1037
+ (controller, aborted, abortCallbacks, encoder) => ({
1038
+ async write(data) {
1039
+ if (aborted.value) return;
1040
+ const chunk = typeof data === "string" ? encoder.encode(data) : data;
1041
+ controller.enqueue(chunk);
1042
+ },
1043
+ async pipe(stream) {
1044
+ if (aborted.value) return;
1045
+ const reader = stream.getReader();
1046
+ try {
1047
+ while (true) {
1048
+ const { done, value } = await reader.read();
1049
+ if (done || aborted.value) break;
1050
+ controller.enqueue(value);
1051
+ }
1052
+ } finally {
1053
+ reader.releaseLock();
1054
+ }
1055
+ },
1056
+ sleep(ms) {
1057
+ return new Promise((resolve) => setTimeout(resolve, ms));
1058
+ },
1059
+ onAbort(callback2) {
1060
+ abortCallbacks.push(callback2);
1061
+ }
1062
+ }),
1063
+ callback,
1064
+ onError
1065
+ );
1066
+ }
1067
+ /**
1068
+ * Text streaming helper with proper headers
1069
+ * @param callback Callback function that receives a TextStreamHelper
1070
+ * @param onError Optional error handler
1071
+ */
1072
+ streamText(callback, onError) {
1073
+ const headers = new Headers(this.response.headers);
1074
+ headers.set("Content-Type", "text/plain; charset=utf-8");
1075
+ headers.set("Transfer-Encoding", "chunked");
1076
+ headers.set("X-Content-Type-Options", "nosniff");
1077
+ return this.createStreamHelper(
1078
+ (controller, aborted, abortCallbacks, encoder) => ({
1079
+ async write(text) {
1080
+ if (aborted.value) return;
1081
+ controller.enqueue(encoder.encode(text));
1082
+ },
1083
+ async writeln(text) {
1084
+ if (aborted.value) return;
1085
+ controller.enqueue(encoder.encode(text + "\n"));
1086
+ },
1087
+ sleep(ms) {
1088
+ return new Promise((resolve) => setTimeout(resolve, ms));
1089
+ },
1090
+ onAbort(callback2) {
1091
+ abortCallbacks.push(callback2);
1092
+ }
1093
+ }),
1094
+ callback,
1095
+ onError,
1096
+ headers
1097
+ );
1098
+ }
1099
+ /**
1100
+ * Server-Sent Events (SSE) streaming helper
1101
+ * @param callback Callback function that receives an SSEStreamHelper
1102
+ * @param onError Optional error handler
1103
+ */
1104
+ streamSSE(callback, onError) {
1105
+ const headers = new Headers(this.response.headers);
1106
+ headers.set("Content-Type", "text/event-stream");
1107
+ headers.set("Cache-Control", "no-cache");
1108
+ headers.set("Connection", "keep-alive");
1109
+ return this.createStreamHelper(
1110
+ (controller, aborted, abortCallbacks, encoder) => ({
1111
+ async writeSSE(message) {
1112
+ if (aborted.value) return;
1113
+ let sseMessage = "";
1114
+ if (message.event) {
1115
+ sseMessage += `event: ${message.event}
1116
+ `;
1117
+ }
1118
+ if (message.id !== void 0) {
1119
+ sseMessage += `id: ${message.id}
1120
+ `;
1121
+ }
1122
+ if (message.retry !== void 0) {
1123
+ sseMessage += `retry: ${message.retry}
1124
+ `;
1125
+ }
1126
+ const dataLines = message.data.split("\n");
1127
+ for (const line of dataLines) {
1128
+ sseMessage += `data: ${line}
1129
+ `;
1130
+ }
1131
+ sseMessage += "\n";
1132
+ controller.enqueue(encoder.encode(sseMessage));
1133
+ },
1134
+ sleep(ms) {
1135
+ return new Promise((resolve) => setTimeout(resolve, ms));
1136
+ },
1137
+ onAbort(callback2) {
1138
+ abortCallbacks.push(callback2);
1139
+ }
1140
+ }),
1141
+ callback,
1142
+ onError,
1143
+ headers
1144
+ );
1145
+ }
852
1146
  }
853
1147
  const compose = (middleware) => {
854
1148
  if (!middleware.length) {
@@ -865,31 +1159,114 @@ const compose = (middleware) => {
865
1159
  return next ? next() : Promise.resolve();
866
1160
  }
867
1161
  const fn = middleware[i];
868
- if (!context[$debug]) {
869
- return fn(context, () => runner(i + 1));
1162
+ if (typeof fn !== "function") {
1163
+ const name = fn?.constructor?.name;
1164
+ console.error(`[Middleware Error] Item at index ${i} is not a function! It is: ${typeof fn} (${name})`, fn);
1165
+ throw new TypeError(`Middleware at index ${i} must be a function, got ${name}`);
1166
+ }
1167
+ const trackingEnabled = context.app?.applicationConfig?.enableMiddlewareTracking;
1168
+ const meta = fn.metadata;
1169
+ let trackingStartTime = 0;
1170
+ if (trackingEnabled && meta) {
1171
+ trackingStartTime = performance.now();
1172
+ context.handlerStack.push({
1173
+ name: meta.name || fn.name || "anonymous",
1174
+ file: meta.file,
1175
+ line: meta.line,
1176
+ isBuiltin: meta.isBuiltin,
1177
+ startTime: trackingStartTime,
1178
+ duration: -1
1179
+ });
870
1180
  }
871
1181
  const debug = context[$debug];
872
- const debugId = fn._debugId || fn.name || "anonymous";
873
- const previousNode = debug.getCurrentNode();
874
- debug.trackEdge(previousNode, debugId);
875
- debug.setNode(debugId);
876
- const start = performance.now();
1182
+ let debugId;
1183
+ let previousNode;
1184
+ let debugStart = 0;
1185
+ if (debug) {
1186
+ debugId = fn._debugId || fn.name || "anonymous";
1187
+ previousNode = debug.getCurrentNode();
1188
+ debug.trackEdge(previousNode, debugId);
1189
+ debug.setNode(debugId);
1190
+ debugStart = performance.now();
1191
+ }
877
1192
  try {
878
- const res = await Promise.resolve(fn(context, () => runner(i + 1)));
879
- debug.trackStep(debugId, "middleware", performance.now() - start, "success");
1193
+ const res = await fn(context, () => runner(i + 1));
1194
+ if (trackingEnabled && meta) {
1195
+ const duration = performance.now() - trackingStartTime;
1196
+ const stackItem = context.handlerStack[context.handlerStack.length - 1];
1197
+ if (stackItem) stackItem.duration = duration;
1198
+ Promise.resolve().then(async () => {
1199
+ try {
1200
+ const db = context.app?.db;
1201
+ if (!db) return;
1202
+ const timestamp = Date.now();
1203
+ await db.upsert(new surrealdb.RecordId("middleware_tracking", {
1204
+ timestamp,
1205
+ name: meta.name
1206
+ }), {
1207
+ name: meta.name,
1208
+ path: context.path,
1209
+ timestamp,
1210
+ duration,
1211
+ file: meta.file,
1212
+ line: meta.line,
1213
+ error: void 0,
1214
+ metadata: {
1215
+ isBuiltin: meta.isBuiltin,
1216
+ pluginName: meta.pluginName
1217
+ }
1218
+ });
1219
+ } catch (e) {
1220
+ }
1221
+ });
1222
+ }
1223
+ if (debug) {
1224
+ debug.trackStep(debugId, "middleware", performance.now() - debugStart, "success");
1225
+ }
880
1226
  return res;
881
1227
  } catch (err) {
882
- debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
883
- return Promise.reject(err);
1228
+ if (trackingEnabled && meta) {
1229
+ const duration = performance.now() - trackingStartTime;
1230
+ const stackItem = context.handlerStack[context.handlerStack.length - 1];
1231
+ if (stackItem) stackItem.duration = duration;
1232
+ Promise.resolve().then(async () => {
1233
+ try {
1234
+ const db = context.app?.db;
1235
+ if (!db) return;
1236
+ const timestamp = Date.now();
1237
+ await db.upsert(new surrealdb.RecordId("middleware_tracking", {
1238
+ timestamp,
1239
+ name: meta.name
1240
+ }), {
1241
+ name: meta.name,
1242
+ path: context.path,
1243
+ timestamp,
1244
+ duration,
1245
+ file: meta.file,
1246
+ line: meta.line,
1247
+ error: String(err),
1248
+ metadata: {
1249
+ isBuiltin: meta.isBuiltin,
1250
+ pluginName: meta.pluginName
1251
+ }
1252
+ });
1253
+ } catch (e) {
1254
+ }
1255
+ });
1256
+ }
1257
+ if (debug) {
1258
+ debug.trackStep(debugId, "middleware", performance.now() - debugStart, "error", err);
1259
+ }
1260
+ throw err;
884
1261
  } finally {
885
- if (previousNode) debug.setNode(previousNode);
1262
+ if (debug && previousNode) debug.setNode(previousNode);
886
1263
  }
887
1264
  }
888
1265
  return runner(0);
889
1266
  };
890
1267
  };
891
1268
  function isObject(item) {
892
- return item && typeof item === "object" && !Array.isArray(item);
1269
+ return !!(item && typeof item === "object" && !Array.isArray(item));
893
1270
  }
894
1271
  function deepMerge(target, ...sources) {
895
1272
  if (!sources.length) return target;
@@ -1154,7 +1531,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1154
1531
  let astMiddlewareRegistry = {};
1155
1532
  let applications = [];
1156
1533
  try {
1157
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BAhvpNY_.cjs"));
1534
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BOtveWL-.cjs"));
1158
1535
  const entrypoint = rootRouter.metadata?.file;
1159
1536
  const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
1160
1537
  const analysisResult = await analyzer2.analyze();
@@ -1206,7 +1583,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1206
1583
  isBuiltinPlugin = true;
1207
1584
  pluginName = router.metadata.pluginName;
1208
1585
  tag = pluginName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1209
- } else if (router.metadata?.file && router.metadata.file.includes("plugins/application/")) {
1586
+ } else if (router.metadata?.file && router.metadata.file.includes("plugins/application/") && !router.metadata.file.match(/\.(spec|test)\.ts$/)) {
1210
1587
  isBuiltinPlugin = true;
1211
1588
  const match = router.metadata.file.match(/plugins\/application\/([^/]+)/);
1212
1589
  if (match) {
@@ -1364,7 +1741,39 @@ async function generateOpenApi(rootRouter, options = {}) {
1364
1741
  const params = [];
1365
1742
  if (astMatch.requestTypes?.query) {
1366
1743
  for (const [name, _type] of Object.entries(astMatch.requestTypes.query)) {
1367
- params.push({ name, in: "query", schema: { type: "string" } });
1744
+ let type = "string";
1745
+ let format;
1746
+ if (_type === "integer") {
1747
+ type = "integer";
1748
+ format = "int32";
1749
+ } else if (_type === "number") {
1750
+ type = "number";
1751
+ format = "float";
1752
+ } else if (_type === "boolean") type = "boolean";
1753
+ const schema = { type };
1754
+ if (format) schema.format = format;
1755
+ params.push({ name, in: "query", schema });
1756
+ }
1757
+ }
1758
+ if (astMatch.requestTypes?.params) {
1759
+ for (const [name, _type] of Object.entries(astMatch.requestTypes.params)) {
1760
+ let type = "string";
1761
+ let format;
1762
+ if (_type === "integer") {
1763
+ type = "integer";
1764
+ format = "int32";
1765
+ } else if (_type === "number") {
1766
+ type = "number";
1767
+ format = "float";
1768
+ } else if (_type === "boolean") type = "boolean";
1769
+ const schema = { type };
1770
+ if (format) schema.format = format;
1771
+ params.push({ name, in: "path", required: true, schema });
1772
+ }
1773
+ }
1774
+ if (astMatch.requestTypes?.headers) {
1775
+ for (const [name, _type] of Object.entries(astMatch.requestTypes.headers)) {
1776
+ params.push({ name, in: "header", schema: { type: "string" } });
1368
1777
  }
1369
1778
  }
1370
1779
  if (params.length > 0) {
@@ -1422,9 +1831,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1422
1831
  const mergedParams = [...existingParams];
1423
1832
  pathParams.forEach((p) => {
1424
1833
  const idx = mergedParams.findIndex((ep) => ep.in === "path" && ep.name === p.name);
1425
- if (idx >= 0) {
1426
- mergedParams[idx] = deepMerge(mergedParams[idx], p);
1427
- } else {
1834
+ if (idx === -1) {
1428
1835
  mergedParams.push(p);
1429
1836
  }
1430
1837
  });
@@ -1697,8 +2104,11 @@ function serveStatic(config, prefix) {
1697
2104
  if (typeof Bun !== "undefined") {
1698
2105
  response = new Response(Bun.file(finalPath));
1699
2106
  } else {
1700
- const fileBuffer = await promises$1.readFile(finalPath, { encoding: "binary" });
1701
- response = new Response(fileBuffer);
2107
+ const { createReadStream } = await import("node:fs");
2108
+ const { Readable } = await import("node:stream");
2109
+ const fileStream = createReadStream(finalPath);
2110
+ const webStream = Readable.toWeb(fileStream);
2111
+ response = new Response(webStream);
1702
2112
  }
1703
2113
  if (config.hooks?.onResponse) {
1704
2114
  const hooked = await config.hooks.onResponse(ctx, response);
@@ -1710,51 +2120,77 @@ function serveStatic(config, prefix) {
1710
2120
  serveStaticMiddleware.pluginName = "ServeStatic";
1711
2121
  return serveStaticMiddleware;
1712
2122
  }
1713
- class Container {
1714
- static services = /* @__PURE__ */ new Map();
1715
- static register(target, instance) {
1716
- this.services.set(target, instance);
1717
- }
1718
- static get(target) {
1719
- return this.services.get(target);
1720
- }
1721
- static has(target) {
1722
- return this.services.has(target);
2123
+ class OpenTelemetryPlugin {
2124
+ constructor(options = {}) {
2125
+ this.options = options;
1723
2126
  }
1724
- static resolve(target) {
1725
- if (this.services.has(target)) {
1726
- return this.services.get(target);
2127
+ api;
2128
+ sdk;
2129
+ async onInit(app) {
2130
+ try {
2131
+ this.api = await import("@opentelemetry/api");
2132
+ } catch (e) {
2133
+ console.warn("OpenTelemetry API not found. OpenTelemetryPlugin will be disabled.");
2134
+ return;
2135
+ }
2136
+ if (this.options.enableAutoInstrumentation !== false) {
2137
+ app.use(this.middleware());
1727
2138
  }
1728
- const instance = new target();
1729
- this.services.set(target, instance);
1730
- return instance;
2139
+ }
2140
+ middleware() {
2141
+ return async (ctx, next) => {
2142
+ if (!this.api) return next();
2143
+ const tracer = this.api.trace.getTracer("shokupan");
2144
+ return tracer.startActiveSpan(`${ctx.req.method} ${ctx.req.path}`, {
2145
+ kind: this.api.SpanKind.SERVER,
2146
+ attributes: {
2147
+ "http.method": ctx.req.method,
2148
+ "http.url": ctx.req.url,
2149
+ "http.host": ctx.req.host,
2150
+ "http.user_agent": ctx.req.headers.get("user-agent") || void 0
2151
+ }
2152
+ }, async (span) => {
2153
+ try {
2154
+ const res = await next();
2155
+ span.setAttributes({
2156
+ "http.status_code": ctx.res.status
2157
+ });
2158
+ if (ctx.res.status >= 500) {
2159
+ span.setStatus({ code: this.api.SpanStatusCode.ERROR });
2160
+ } else {
2161
+ span.setStatus({ code: this.api.SpanStatusCode.OK });
2162
+ }
2163
+ return res;
2164
+ } catch (err) {
2165
+ span.recordException(err);
2166
+ span.setStatus({ code: this.api.SpanStatusCode.ERROR, message: err.message });
2167
+ throw err;
2168
+ } finally {
2169
+ span.end();
2170
+ }
2171
+ });
2172
+ };
1731
2173
  }
1732
2174
  }
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
- }
1746
- const tracer = api.trace.getTracer("shokupan.middleware");
1747
- function traceHandler(fn, name) {
1748
- return async function(...args) {
1749
- return tracer.startActiveSpan(`route handler - ${name}`, {
2175
+ function traceMiddleware(fn, name) {
2176
+ let api;
2177
+ try {
2178
+ api = require("@opentelemetry/api");
2179
+ } catch {
2180
+ }
2181
+ if (!api) return fn;
2182
+ const tracer = api.trace.getTracer("shokupan.middleware");
2183
+ const middlewareName = name || fn.name || "anonymous middleware";
2184
+ return async (ctx, next) => {
2185
+ return tracer.startActiveSpan(`middleware - ${middlewareName}`, {
1750
2186
  kind: api.SpanKind.INTERNAL,
1751
2187
  attributes: {
1752
- "http.route": name,
1753
- "component": "shokupan.route"
2188
+ "code.function": middlewareName,
2189
+ "component": "shokupan.middleware"
1754
2190
  }
1755
2191
  }, async (span) => {
1756
2192
  try {
1757
- const result = await fn.apply(this, args);
2193
+ const result = await fn(ctx, next);
1758
2194
  return result;
1759
2195
  } catch (err) {
1760
2196
  span.recordException(err);
@@ -1766,9 +2202,183 @@ function traceHandler(fn, name) {
1766
2202
  });
1767
2203
  };
1768
2204
  }
1769
- function getCallerInfo(skipFrames = 1) {
1770
- let file = "unknown";
1771
- let line = 0;
2205
+ function traceHandler(fn, name) {
2206
+ let api;
2207
+ try {
2208
+ api = require("@opentelemetry/api");
2209
+ } catch {
2210
+ }
2211
+ if (!api) return fn;
2212
+ const tracer = api.trace.getTracer("shokupan.middleware");
2213
+ return async function(...args) {
2214
+ return tracer.startActiveSpan(`route handler - ${name}`, {
2215
+ kind: api.SpanKind.INTERNAL,
2216
+ attributes: {
2217
+ "http.route": name,
2218
+ "component": "shokupan.route"
2219
+ }
2220
+ }, async (span) => {
2221
+ try {
2222
+ const result = await fn.apply(this, args);
2223
+ return result;
2224
+ } catch (err) {
2225
+ span.recordException(err);
2226
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
2227
+ throw err;
2228
+ } finally {
2229
+ span.end();
2230
+ }
2231
+ });
2232
+ };
2233
+ }
2234
+ class ResilienceFactory {
2235
+ static createPolicy(config) {
2236
+ const policies = [];
2237
+ if (config.retry) {
2238
+ const builder = cockatiel.handleAll;
2239
+ let retries = (config.retry.attempts ?? 3) - 1;
2240
+ if (retries < 0) retries = 0;
2241
+ let retryPolicy;
2242
+ if (config.retry.backoff === "exponential") {
2243
+ retryPolicy = cockatiel.retry(builder, {
2244
+ maxAttempts: retries,
2245
+ backoff: new cockatiel.ExponentialBackoff({
2246
+ initialDelay: config.retry.delay || 1e3,
2247
+ maxDelay: config.retry.maxDelay || 3e4
2248
+ })
2249
+ });
2250
+ } else {
2251
+ retryPolicy = cockatiel.retry(builder, {
2252
+ maxAttempts: retries,
2253
+ backoff: new cockatiel.ConstantBackoff(config.retry.delay || 1e3)
2254
+ });
2255
+ }
2256
+ policies.push(retryPolicy);
2257
+ }
2258
+ if (config.circuitBreaker) {
2259
+ const builder = cockatiel.handleAll;
2260
+ const breaker = cockatiel.circuitBreaker(builder, {
2261
+ halfOpenAfter: config.circuitBreaker.resetTimeout || 1e4,
2262
+ breaker: new cockatiel.ConsecutiveBreaker(config.circuitBreaker.threshold || 5)
2263
+ });
2264
+ policies.push(breaker);
2265
+ }
2266
+ if (config.timeout) {
2267
+ policies.push(cockatiel.timeout(config.timeout, { strategy: cockatiel.TimeoutStrategy.Aggressive, abortOnReturn: true }));
2268
+ }
2269
+ if (config.bulkhead) {
2270
+ policies.push(cockatiel.bulkhead(config.bulkhead));
2271
+ }
2272
+ if (config.fallback !== void 0) {
2273
+ const builder = cockatiel.handleAll;
2274
+ const fb = cockatiel.fallback(builder, config.fallback);
2275
+ policies.push(fb);
2276
+ }
2277
+ if (policies.length === 0) {
2278
+ return { execute: (fn) => fn() };
2279
+ }
2280
+ return cockatiel.wrap(...policies.reverse());
2281
+ }
2282
+ }
2283
+ const metadataStore = /* @__PURE__ */ new WeakMap();
2284
+ function defineMetadata(key, value, target, propertyKey) {
2285
+ let targetMetadata = metadataStore.get(target);
2286
+ if (!targetMetadata) {
2287
+ targetMetadata = /* @__PURE__ */ new Map();
2288
+ metadataStore.set(target, targetMetadata);
2289
+ }
2290
+ const storageKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : key;
2291
+ targetMetadata.set(storageKey, value);
2292
+ }
2293
+ function getMetadata(key, target, propertyKey) {
2294
+ const targetMetadata = metadataStore.get(target);
2295
+ if (!targetMetadata) return void 0;
2296
+ const storageKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : key;
2297
+ return targetMetadata.get(storageKey);
2298
+ }
2299
+ if (typeof Reflect === "object") {
2300
+ if (!Reflect.defineMetadata) {
2301
+ Reflect.defineMetadata = defineMetadata;
2302
+ }
2303
+ if (!Reflect.getMetadata) {
2304
+ Reflect.getMetadata = getMetadata;
2305
+ }
2306
+ if (!Reflect.metadata) {
2307
+ Reflect.metadata = function(metadataKey, metadataValue) {
2308
+ return function decorator(target, propertyKey) {
2309
+ defineMetadata(metadataKey, metadataValue, target, propertyKey);
2310
+ };
2311
+ };
2312
+ }
2313
+ }
2314
+ class Container {
2315
+ static services = /* @__PURE__ */ new Map();
2316
+ static register(target, instance) {
2317
+ this.services.set(target, instance);
2318
+ }
2319
+ static get(target) {
2320
+ return this.services.get(target);
2321
+ }
2322
+ static has(target) {
2323
+ return this.services.has(target);
2324
+ }
2325
+ static cache = /* @__PURE__ */ new Map();
2326
+ static resolvingStack = /* @__PURE__ */ new Set();
2327
+ static resolve(target) {
2328
+ if (this.services.has(target)) {
2329
+ return this.services.get(target);
2330
+ }
2331
+ if (this.resolvingStack.has(target)) {
2332
+ const cycle = Array.from(this.resolvingStack);
2333
+ cycle.push(target);
2334
+ throw new Error(`Circular dependency detected: ${cycle.map((t) => t.name || t).join(" -> ")}`);
2335
+ }
2336
+ this.resolvingStack.add(target);
2337
+ try {
2338
+ let meta = this.cache.get(target);
2339
+ if (!meta) {
2340
+ const scope = Reflect.getMetadata("di:scope", target) || "singleton";
2341
+ const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
2342
+ const manualTokens = Reflect.getMetadata("di:constructor:params", target) || [];
2343
+ const dependencies = paramTypes.map((param, index) => {
2344
+ const manual = manualTokens.find((t) => t.index === index);
2345
+ if (manual && manual.token) return manual.token;
2346
+ if (param === String || param === Number || param === Boolean || param === Object || param === void 0) return void 0;
2347
+ return param;
2348
+ });
2349
+ meta = { scope, dependencies };
2350
+ this.cache.set(target, meta);
2351
+ }
2352
+ const args = meta.dependencies.map((dep) => dep ? Container.resolve(dep) : void 0);
2353
+ const instance = new target(...args);
2354
+ if (typeof instance.onInit === "function") {
2355
+ instance.onInit();
2356
+ }
2357
+ if (meta.scope === "singleton") {
2358
+ this.services.set(target, instance);
2359
+ }
2360
+ return instance;
2361
+ } finally {
2362
+ this.resolvingStack.delete(target);
2363
+ }
2364
+ }
2365
+ static async teardown() {
2366
+ for (const [target, instance] of this.services.entries()) {
2367
+ if (typeof instance.onDestroy === "function") {
2368
+ await instance.onDestroy();
2369
+ }
2370
+ }
2371
+ this.services.clear();
2372
+ this.cache.clear();
2373
+ }
2374
+ }
2375
+ const di = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2376
+ __proto__: null,
2377
+ Container
2378
+ }, Symbol.toStringTag, { value: "Module" }));
2379
+ function getCallerInfo(skipFrames = 1) {
2380
+ let file = "unknown";
2381
+ let line = 0;
1772
2382
  try {
1773
2383
  const err = new Error();
1774
2384
  const stack = err.stack?.split("\n") || [];
@@ -1783,6 +2393,7 @@ function getCallerInfo(skipFrames = 1) {
1783
2393
  if (l.includes("src/router.ts")) continue;
1784
2394
  if (l.includes("src/util/decorators.ts")) continue;
1785
2395
  if (l.includes("src/shokupan.ts")) continue;
2396
+ if (l.includes("src/plugins/application/openapi/openapi.ts")) continue;
1786
2397
  found++;
1787
2398
  if (found >= skipFrames) {
1788
2399
  const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
@@ -1805,6 +2416,7 @@ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1805
2416
  RouteParamType2["HEADER"] = "HEADER";
1806
2417
  RouteParamType2["REQUEST"] = "REQUEST";
1807
2418
  RouteParamType2["CONTEXT"] = "CONTEXT";
2419
+ RouteParamType2["SERVICE"] = "SERVICE";
1808
2420
  return RouteParamType2;
1809
2421
  })(RouteParamType || {});
1810
2422
  class ControllerScanner {
@@ -1836,7 +2448,7 @@ class ControllerScanner {
1836
2448
  line: info.line,
1837
2449
  name: instance.constructor.name
1838
2450
  };
1839
- router.registerControllerInstance(instance);
2451
+ router.bindController(instance);
1840
2452
  const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1841
2453
  const proto = Object.getPrototypeOf(instance);
1842
2454
  const methods = /* @__PURE__ */ new Set();
@@ -1850,6 +2462,10 @@ class ControllerScanner {
1850
2462
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1851
2463
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1852
2464
  const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
2465
+ const mcpTools = instance[$mcpTools] || proto && proto[$mcpTools];
2466
+ const mcpPrompts = instance[$mcpPrompts] || proto && proto[$mcpPrompts];
2467
+ const mcpResources = instance[$mcpResources] || proto && proto[$mcpResources];
2468
+ const resilienceConfigMap = instance[$resilienceConfig] || proto && proto[$resilienceConfig];
1853
2469
  let routesAttached = 0;
1854
2470
  for (let i = 0; i < Array.from(methods).length; i++) {
1855
2471
  const name = Array.from(methods)[i];
@@ -1958,6 +2574,9 @@ class ControllerScanner {
1958
2574
  case RouteParamType.CONTEXT:
1959
2575
  args[arg.index] = ctx;
1960
2576
  break;
2577
+ case RouteParamType.SERVICE:
2578
+ args[arg.index] = Container.resolve(arg.token);
2579
+ break;
1961
2580
  }
1962
2581
  }
1963
2582
  }
@@ -1971,6 +2590,14 @@ class ControllerScanner {
1971
2590
  return composed(ctx, () => wrappedHandler(ctx));
1972
2591
  };
1973
2592
  }
2593
+ const config = resilienceConfigMap?.get(name);
2594
+ if (config) {
2595
+ const policy = ResilienceFactory.createPolicy(config);
2596
+ const baseHandler = finalHandler;
2597
+ finalHandler = async (ctx) => {
2598
+ return policy.execute(() => baseHandler(ctx));
2599
+ };
2600
+ }
1974
2601
  finalHandler.originalHandler = originalHandler;
1975
2602
  if (finalHandler !== wrappedHandler) {
1976
2603
  wrappedHandler.originalHandler = originalHandler;
@@ -2027,6 +2654,25 @@ class ControllerScanner {
2027
2654
  wrappedHandler.originalHandler = originalHandler;
2028
2655
  router.event(eventConfig.eventName, wrappedHandler);
2029
2656
  }
2657
+ const toolConfig = mcpTools?.get(name);
2658
+ if (toolConfig) {
2659
+ const handler = originalHandler.bind(instance);
2660
+ router.tool(toolConfig.name || name, toolConfig.inputSchema, handler);
2661
+ }
2662
+ const promptConfig = mcpPrompts?.get(name);
2663
+ if (promptConfig) {
2664
+ const handler = originalHandler.bind(instance);
2665
+ router.prompt(promptConfig.name || name, promptConfig.arguments, handler);
2666
+ }
2667
+ const resourceConfig = mcpResources?.get(name);
2668
+ if (resourceConfig) {
2669
+ const handler = originalHandler.bind(instance);
2670
+ router.resource(resourceConfig.uri, {
2671
+ name: resourceConfig.name || name,
2672
+ description: resourceConfig.description,
2673
+ mimeType: resourceConfig.mimeType
2674
+ }, handler);
2675
+ }
2030
2676
  }
2031
2677
  if (routesAttached === 0) {
2032
2678
  console.warn(`No routes attached to controller ${instance.constructor.name}`);
@@ -2057,71 +2703,177 @@ function getErrorStatus(err) {
2057
2703
  }
2058
2704
  return 500;
2059
2705
  }
2706
+ class NotFoundError extends HttpError {
2707
+ constructor(message = "Not Found") {
2708
+ super(message, 404);
2709
+ this.name = "NotFoundError";
2710
+ }
2711
+ }
2060
2712
  class EventError extends HttpError {
2061
2713
  constructor(message = "Event Error") {
2062
2714
  super(message, 500);
2063
2715
  this.name = "EventError";
2064
2716
  }
2065
2717
  }
2718
+ class McpProtocol {
2719
+ tools = /* @__PURE__ */ new Map();
2720
+ prompts = /* @__PURE__ */ new Map();
2721
+ resources = /* @__PURE__ */ new Map();
2722
+ constructor(tools = [], prompts = [], resources = []) {
2723
+ tools.forEach((t) => this.tools.set(t.name, t));
2724
+ prompts.forEach((p) => this.prompts.set(p.name, p));
2725
+ resources.forEach((r) => this.resources.set(r.uri, r));
2726
+ }
2727
+ addTool(tool) {
2728
+ this.tools.set(tool.name, tool);
2729
+ }
2730
+ addPrompt(prompt) {
2731
+ this.prompts.set(prompt.name, prompt);
2732
+ }
2733
+ addResource(resource) {
2734
+ this.resources.set(resource.uri, resource);
2735
+ }
2736
+ merge(other) {
2737
+ other.tools.forEach((t) => this.tools.set(t.name, t));
2738
+ other.prompts.forEach((p) => this.prompts.set(p.name, p));
2739
+ other.resources.forEach((r) => this.resources.set(r.uri, r));
2740
+ }
2741
+ async handleMessage(message) {
2742
+ if (message.jsonrpc !== "2.0") {
2743
+ return this.error(message.id, -32600, "Invalid Request");
2744
+ }
2745
+ try {
2746
+ switch (message.method) {
2747
+ case "initialize":
2748
+ return this.success(message.id, {
2749
+ protocolVersion: "2024-11-05",
2750
+ serverInfo: {
2751
+ name: "Shokupan MCP",
2752
+ version: "1.0.0"
2753
+ },
2754
+ capabilities: {
2755
+ tools: this.tools.size > 0 ? {} : void 0,
2756
+ prompts: this.prompts.size > 0 ? {} : void 0,
2757
+ resources: this.resources.size > 0 ? {} : void 0
2758
+ }
2759
+ });
2760
+ case "ping":
2761
+ return this.success(message.id, {});
2762
+ case "tools/list":
2763
+ if (this.tools.size === 0) return this.success(message.id, { tools: [] });
2764
+ return this.success(message.id, {
2765
+ tools: Array.from(this.tools.values()).map((t) => ({
2766
+ name: t.name,
2767
+ description: t.description,
2768
+ inputSchema: t.inputSchema || { type: "object", properties: {} }
2769
+ }))
2770
+ });
2771
+ case "tools/call": {
2772
+ if (!message.params || !message.params.name) {
2773
+ return this.error(message.id, -32602, "Invalid params: name required");
2774
+ }
2775
+ const tool = this.tools.get(message.params.name);
2776
+ if (!tool) {
2777
+ return this.error(message.id, -32601, `Tool not found: ${message.params.name}`);
2778
+ }
2779
+ try {
2780
+ const result = await tool.handler(message.params.arguments || {});
2781
+ return this.success(message.id, result);
2782
+ } catch (e) {
2783
+ return {
2784
+ jsonrpc: "2.0",
2785
+ id: message.id ?? null,
2786
+ result: {
2787
+ isError: true,
2788
+ content: [{ type: "text", text: e.message || String(e) }]
2789
+ }
2790
+ };
2791
+ }
2792
+ }
2793
+ case "prompts/list":
2794
+ if (this.prompts.size === 0) return this.success(message.id, { prompts: [] });
2795
+ return this.success(message.id, {
2796
+ prompts: Array.from(this.prompts.values()).map((p) => ({
2797
+ name: p.name,
2798
+ description: p.description,
2799
+ arguments: p.arguments
2800
+ }))
2801
+ });
2802
+ case "prompts/get": {
2803
+ if (!message.params || !message.params.name) {
2804
+ return this.error(message.id, -32602, "Invalid params: name required");
2805
+ }
2806
+ const prompt = this.prompts.get(message.params.name);
2807
+ if (!prompt) {
2808
+ return this.error(message.id, -32601, `Prompt not found: ${message.params.name}`);
2809
+ }
2810
+ const result = await prompt.handler(message.params.arguments || {});
2811
+ return this.success(message.id, result);
2812
+ }
2813
+ case "resources/list":
2814
+ if (this.resources.size === 0) return this.success(message.id, { resources: [] });
2815
+ return this.success(message.id, {
2816
+ resources: Array.from(this.resources.values()).map((r) => ({
2817
+ uri: r.uri,
2818
+ name: r.name,
2819
+ description: r.description,
2820
+ mimeType: r.mimeType
2821
+ }))
2822
+ });
2823
+ case "resources/read": {
2824
+ if (!message.params || !message.params.uri) {
2825
+ return this.error(message.id, -32602, "Invalid params: uri required");
2826
+ }
2827
+ let resource = this.resources.get(message.params.uri);
2828
+ if (!resource) {
2829
+ return this.error(message.id, -32601, `Resource not found: ${message.params.uri}`);
2830
+ }
2831
+ const result = await resource.handler(message.params.uri);
2832
+ return this.success(message.id, result);
2833
+ }
2834
+ default:
2835
+ if (message.id === void 0) return null;
2836
+ return this.error(message.id, -32601, "Method not found");
2837
+ }
2838
+ } catch (err) {
2839
+ return this.error(message.id, -32603, "Internal Error", err.message);
2840
+ }
2841
+ }
2842
+ success(id, result) {
2843
+ return {
2844
+ jsonrpc: "2.0",
2845
+ id: id ?? null,
2846
+ result
2847
+ };
2848
+ }
2849
+ error(id, code, message, data) {
2850
+ return {
2851
+ jsonrpc: "2.0",
2852
+ id: id ?? null,
2853
+ error: { code, message, data }
2854
+ };
2855
+ }
2856
+ }
2066
2857
  class MiddlewareTracker {
2067
2858
  static wrap(handler, context) {
2068
2859
  const { file, line, name, isBuiltin, pluginName } = context;
2069
2860
  const handlerName = name || handler.name || "anonymous";
2070
- const trackedHandler = async (ctx, next) => {
2071
- if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2072
- return handler(ctx, next);
2073
- }
2074
- const startTime = performance.now();
2075
- let error = void 0;
2076
- try {
2077
- ctx.handlerStack.push({
2078
- name: handlerName,
2079
- file,
2080
- line,
2081
- isBuiltin,
2082
- startTime,
2083
- duration: -1
2084
- });
2085
- return await handler(ctx, next);
2086
- } catch (e) {
2087
- error = e;
2088
- throw e;
2089
- } finally {
2090
- const duration = performance.now() - startTime;
2091
- const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
2092
- if (stackItem && stackItem.name === handlerName) {
2093
- stackItem.duration = duration;
2861
+ try {
2862
+ handler.metadata = context;
2863
+ if (!handler.name || handler.name === "anonymous") {
2864
+ try {
2865
+ Object.defineProperty(handler, "name", { value: handlerName, configurable: true });
2866
+ } catch (e) {
2094
2867
  }
2095
- Promise.resolve().then(async () => {
2096
- try {
2097
- const db = ctx.app?.db;
2098
- if (!db) return;
2099
- const timestamp = Date.now();
2100
- await db.upsert(new surrealdb.RecordId("middleware_tracking", {
2101
- timestamp,
2102
- name: handlerName
2103
- }), {
2104
- name: handlerName,
2105
- path: ctx.path,
2106
- timestamp,
2107
- duration,
2108
- file,
2109
- line,
2110
- error: error ? String(error) : void 0,
2111
- metadata: {
2112
- isBuiltin,
2113
- pluginName
2114
- }
2115
- });
2116
- } catch (err) {
2117
- }
2118
- });
2119
2868
  }
2120
- };
2121
- trackedHandler.metadata = handler.metadata || context;
2122
- Object.defineProperty(trackedHandler, "name", { value: handlerName });
2123
- trackedHandler.originalHandler = handler.originalHandler || handler;
2124
- return trackedHandler;
2869
+ } catch (e) {
2870
+ const wrapped = handler.bind(null);
2871
+ wrapped.metadata = context;
2872
+ Object.defineProperty(wrapped, "name", { value: handlerName });
2873
+ wrapped.originalHandler = handler.originalHandler || handler;
2874
+ return wrapped;
2875
+ }
2876
+ return handler;
2125
2877
  }
2126
2878
  }
2127
2879
  class ShokupanRequestBase {
@@ -2147,6 +2899,15 @@ class ShokupanRequestBase {
2147
2899
  this.headers = new Headers(this.headers);
2148
2900
  }
2149
2901
  }
2902
+ clone() {
2903
+ return new ShokupanRequest({
2904
+ method: this.method,
2905
+ url: this.url,
2906
+ headers: new Headers(this.headers),
2907
+ body: this.body
2908
+ // Shallow copy of body, might need deep copy if object
2909
+ });
2910
+ }
2150
2911
  }
2151
2912
  const ShokupanRequest = ShokupanRequestBase;
2152
2913
  class RouterTrie {
@@ -2312,9 +3073,6 @@ class ShokupanRouter {
2312
3073
  return this._hasAfterValidateHook;
2313
3074
  }
2314
3075
  requestTimeout;
2315
- get db() {
2316
- return this.root?.db;
2317
- }
2318
3076
  hookCache = /* @__PURE__ */ new Map();
2319
3077
  hooksInitialized = false;
2320
3078
  middleware = [];
@@ -2329,6 +3087,7 @@ class ShokupanRouter {
2329
3087
  trie = new RouterTrie();
2330
3088
  metadata;
2331
3089
  // Metadata for the router itself
3090
+ mcpProtocol = new McpProtocol();
2332
3091
  currentGuards = [];
2333
3092
  eventHandlers = /* @__PURE__ */ new Map();
2334
3093
  /**
@@ -2340,7 +3099,7 @@ class ShokupanRouter {
2340
3099
  return this;
2341
3100
  }
2342
3101
  // Registry Accessor
2343
- getComponentRegistry() {
3102
+ get registry() {
2344
3103
  const controllerRoutesMap = /* @__PURE__ */ new Map();
2345
3104
  const localRoutes = [];
2346
3105
  for (let i = 0; i < this[$routes].length; i++) {
@@ -2376,7 +3135,7 @@ class ShokupanRouter {
2376
3135
  type: "router",
2377
3136
  path: r[$mountPath].startsWith("/") ? r[$mountPath] : "/" + r[$mountPath],
2378
3137
  metadata: r.metadata,
2379
- children: r.getComponentRegistry()
3138
+ children: r.registry
2380
3139
  }));
2381
3140
  const controllers = this[$childControllers].map((c) => {
2382
3141
  const routes = controllerRoutesMap.get(c) || [];
@@ -2454,6 +3213,39 @@ class ShokupanRouter {
2454
3213
  handlers.push(handler);
2455
3214
  return this;
2456
3215
  }
3216
+ /**
3217
+ * Registers an MCP Tool.
3218
+ */
3219
+ tool(name, schema, handler) {
3220
+ this.mcpProtocol.addTool({
3221
+ name,
3222
+ inputSchema: schema,
3223
+ handler
3224
+ });
3225
+ return this;
3226
+ }
3227
+ /**
3228
+ * Registers an MCP Prompt.
3229
+ */
3230
+ prompt(name, args, handler) {
3231
+ this.mcpProtocol.addPrompt({
3232
+ name,
3233
+ arguments: args,
3234
+ handler
3235
+ });
3236
+ return this;
3237
+ }
3238
+ /**
3239
+ * Registers an MCP Resource.
3240
+ */
3241
+ resource(uri, options, handler) {
3242
+ this.mcpProtocol.addResource({
3243
+ uri,
3244
+ handler,
3245
+ ...options
3246
+ });
3247
+ return this;
3248
+ }
2457
3249
  /**
2458
3250
  * Finds an event handler(s) by name.
2459
3251
  */
@@ -2471,7 +3263,7 @@ class ShokupanRouter {
2471
3263
  /**
2472
3264
  * Registers a controller instance to the router.
2473
3265
  */
2474
- registerControllerInstance(controller) {
3266
+ bindController(controller) {
2475
3267
  this[$childControllers].push(controller);
2476
3268
  }
2477
3269
  /**
@@ -2752,59 +3544,48 @@ class ShokupanRouter {
2752
3544
  }
2753
3545
  }
2754
3546
  }
2755
- let wrappedHandler = async (ctx) => {
2756
- return handler(ctx);
2757
- };
2758
- wrappedHandler.originalHandler = handler.originalHandler || handler;
2759
- const routeGuards = [...this.currentGuards];
2760
3547
  const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
2761
- if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
2762
- const originalHandler = wrappedHandler;
3548
+ const effectiveRenderer = renderer ?? this.config?.renderer ?? this.rootConfig?.renderer;
3549
+ const routeGuards = [...this.currentGuards];
3550
+ let wrappedHandler = handler;
3551
+ if (effectiveTimeout && effectiveTimeout > 0 || effectiveRenderer || routeGuards.length > 0) {
3552
+ const originalHandler = handler;
2763
3553
  wrappedHandler = async (ctx) => {
2764
- if (ctx.server) {
3554
+ if (effectiveTimeout && effectiveTimeout > 0 && ctx.server) {
2765
3555
  ctx.server.timeout(ctx.req, effectiveTimeout / 1e3);
2766
3556
  }
2767
- return originalHandler(ctx);
2768
- };
2769
- wrappedHandler.originalHandler = originalHandler.originalHandler || originalHandler;
2770
- }
2771
- if (routeGuards.length > 0) {
2772
- const innerHandler = wrappedHandler;
2773
- wrappedHandler = async (ctx) => {
2774
- for (let i = 0; i < routeGuards.length; i++) {
2775
- const guard = routeGuards[i];
2776
- let guardPassed = false;
2777
- let nextCalled = false;
2778
- const next = () => {
2779
- nextCalled = true;
2780
- return Promise.resolve();
2781
- };
2782
- try {
2783
- const result = await guard.handler(ctx, next);
2784
- if (result === true || nextCalled) {
2785
- guardPassed = true;
2786
- } else if (result !== void 0 && result !== null && result !== false) {
2787
- return result;
2788
- } else {
3557
+ if (effectiveRenderer) {
3558
+ ctx.setRenderer(effectiveRenderer);
3559
+ }
3560
+ if (routeGuards.length > 0) {
3561
+ for (let i = 0; i < routeGuards.length; i++) {
3562
+ const guard = routeGuards[i];
3563
+ let guardPassed = false;
3564
+ let nextCalled = false;
3565
+ const next = () => {
3566
+ nextCalled = true;
3567
+ return Promise.resolve();
3568
+ };
3569
+ try {
3570
+ const result = await guard.handler(ctx, next);
3571
+ if (result === true || nextCalled) {
3572
+ guardPassed = true;
3573
+ } else if (result !== void 0 && result !== null && result !== false) {
3574
+ return result;
3575
+ } else {
3576
+ return ctx.json({ error: "Forbidden" }, 403);
3577
+ }
3578
+ } catch (error) {
3579
+ throw error;
3580
+ }
3581
+ if (!guardPassed) {
2789
3582
  return ctx.json({ error: "Forbidden" }, 403);
2790
3583
  }
2791
- } catch (error) {
2792
- throw error;
2793
- }
2794
- if (!guardPassed) {
2795
- return ctx.json({ error: "Forbidden" }, 403);
2796
3584
  }
2797
3585
  }
2798
- return innerHandler(ctx);
2799
- };
2800
- }
2801
- const effectiveRenderer = renderer ?? this.config?.renderer ?? this.rootConfig?.renderer;
2802
- if (effectiveRenderer) {
2803
- const innerHandler = wrappedHandler;
2804
- wrappedHandler = async (ctx) => {
2805
- ctx.setRenderer(effectiveRenderer);
2806
- return innerHandler(ctx);
3586
+ return originalHandler(ctx);
2807
3587
  };
3588
+ wrappedHandler.originalHandler = handler.originalHandler || handler;
2808
3589
  }
2809
3590
  const { file, line } = metadata || getCallerInfo();
2810
3591
  wrappedHandler = MiddlewareTracker.wrap(wrappedHandler, {
@@ -3048,68 +3829,6 @@ class ShokupanRouter {
3048
3829
  }
3049
3830
  }
3050
3831
  }
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
3832
  class BunAdapter {
3114
3833
  server;
3115
3834
  async listen(port, app) {
@@ -3246,32 +3965,150 @@ class BunAdapter {
3246
3965
  class NodeAdapter {
3247
3966
  server;
3248
3967
  async listen(port, app) {
3249
- let factory = app.applicationConfig.serverFactory;
3250
- if (!factory) {
3251
- factory = createHttpServer();
3252
- }
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
3260
- };
3261
- this.server = await factory(serveOptions);
3262
- return this.server;
3263
- }
3264
- async stop() {
3265
- if (this.server?.stop) {
3266
- await this.server.stop();
3968
+ const factory = app.applicationConfig.serverFactory;
3969
+ let nodeServer;
3970
+ if (factory) {
3971
+ const serveOptions = {
3972
+ port,
3973
+ hostname: app.applicationConfig.hostname,
3974
+ development: app.applicationConfig.development,
3975
+ fetch: app.fetch.bind(app),
3976
+ reusePort: app.applicationConfig.reusePort
3977
+ };
3978
+ this.server = await factory(serveOptions);
3979
+ return this.server;
3267
3980
  }
3268
- }
3269
- }
3270
- let fs;
3271
- class DefaultFileSystemAdapter {
3272
- async readFile(path2) {
3273
- if (typeof Bun !== "undefined") {
3274
- return Bun.file(path2);
3981
+ nodeServer = http__namespace.createServer(async (req, res) => {
3982
+ const url = new URL(req.url, `http://${req.headers.host}`);
3983
+ const request = new Request(url.toString(), {
3984
+ method: req.method,
3985
+ headers: req.headers,
3986
+ body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
3987
+ start(controller) {
3988
+ req.on("data", (chunk) => controller.enqueue(chunk));
3989
+ req.on("end", () => controller.close());
3990
+ req.on("error", (err) => controller.error(err));
3991
+ }
3992
+ }),
3993
+ // Required for Node.js undici when sending a body
3994
+ // @ts-ignore
3995
+ duplex: "half"
3996
+ });
3997
+ const response = await app.fetch(request, fauxServer);
3998
+ res.statusCode = response.status;
3999
+ response.headers.forEach((v, k) => res.setHeader(k, v));
4000
+ if (response.body) {
4001
+ const buffer = await response.arrayBuffer();
4002
+ res.end(Buffer.from(buffer));
4003
+ } else {
4004
+ res.end();
4005
+ }
4006
+ });
4007
+ this.server = nodeServer;
4008
+ const fauxServer = {
4009
+ stop: () => {
4010
+ nodeServer.close();
4011
+ return Promise.resolve();
4012
+ },
4013
+ upgrade(req, options) {
4014
+ return false;
4015
+ },
4016
+ reload(options) {
4017
+ return fauxServer;
4018
+ },
4019
+ get port() {
4020
+ const addr = nodeServer.address();
4021
+ if (typeof addr === "object" && addr !== null) {
4022
+ return addr.port;
4023
+ }
4024
+ return port;
4025
+ },
4026
+ hostname: app.applicationConfig.hostname || "localhost",
4027
+ development: app.applicationConfig.development || false,
4028
+ pendingRequests: 0,
4029
+ requestIP: (req) => null,
4030
+ publish: () => 0,
4031
+ subscriberCount: () => 0,
4032
+ url: new URL(`http://${app.applicationConfig.hostname || "localhost"}:${port}`),
4033
+ // Expose the raw Node.js server
4034
+ // @ts-ignore
4035
+ nodeServer
4036
+ };
4037
+ return new Promise((resolve) => {
4038
+ nodeServer.listen(port, app.applicationConfig.hostname, () => {
4039
+ resolve(fauxServer);
4040
+ });
4041
+ });
4042
+ }
4043
+ async stop() {
4044
+ if (this.server?.stop) {
4045
+ await this.server.stop();
4046
+ } else if (this.server?.close) {
4047
+ this.server.close();
4048
+ }
4049
+ }
4050
+ }
4051
+ class ShokupanServer {
4052
+ constructor(app) {
4053
+ this.app = app;
4054
+ }
4055
+ server;
4056
+ adapter;
4057
+ /**
4058
+ * Starts the application server.
4059
+ * @param port The port to listen on.
4060
+ */
4061
+ async listen(port) {
4062
+ const config = this.app.applicationConfig;
4063
+ const finalPort = port ?? config.port ?? 3e3;
4064
+ if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
4065
+ throw new Error("Invalid port number");
4066
+ }
4067
+ await this.app.start();
4068
+ let adapterName = config.adapter;
4069
+ let adapter;
4070
+ if (!adapterName) {
4071
+ if (typeof Bun !== "undefined") {
4072
+ config.adapter = "bun";
4073
+ adapter = new BunAdapter();
4074
+ } else {
4075
+ config.adapter = "node";
4076
+ adapter = new NodeAdapter();
4077
+ }
4078
+ } else if (adapterName === "bun") {
4079
+ adapter = new BunAdapter();
4080
+ } else if (adapterName === "node") {
4081
+ adapter = new NodeAdapter();
4082
+ } else if (adapterName === "wintercg") {
4083
+ throw new Error("WinterCG adapter does not support listen(). Use fetch directly.");
4084
+ } else {
4085
+ adapter = new NodeAdapter();
4086
+ }
4087
+ this.adapter = adapter;
4088
+ this.app.compile();
4089
+ this.server = await adapter.listen(finalPort, this.app);
4090
+ if (finalPort === 0 && this.server?.port) {
4091
+ config.port = this.server.port;
4092
+ }
4093
+ return this.server;
4094
+ }
4095
+ /**
4096
+ * Stops the server.
4097
+ */
4098
+ async stop(closeActiveConnections) {
4099
+ if (this.adapter?.stop) {
4100
+ await this.adapter.stop();
4101
+ } else if (this.server?.stop) {
4102
+ await this.server.stop(closeActiveConnections);
4103
+ }
4104
+ this.server = void 0;
4105
+ }
4106
+ }
4107
+ let fs;
4108
+ class DefaultFileSystemAdapter {
4109
+ async readFile(path2) {
4110
+ if (typeof Bun !== "undefined") {
4111
+ return Bun.file(path2);
3275
4112
  } else {
3276
4113
  fs ??= await import("node:fs/promises");
3277
4114
  return fs.readFile(path2);
@@ -3449,12 +4286,39 @@ class SurrealDatastore {
3449
4286
  return this.db.close();
3450
4287
  }
3451
4288
  }
4289
+ const kContext = /* @__PURE__ */ Symbol("kContext");
4290
+ let patched = false;
4291
+ function enablePromisePatch() {
4292
+ if (patched) return;
4293
+ patched = true;
4294
+ const OriginalPromise = global.Promise;
4295
+ global.Promise = class PatchedPromise extends OriginalPromise {
4296
+ [kContext];
4297
+ constructor(executor) {
4298
+ const store = asyncContext.getStore();
4299
+ const stack = new Error().stack || "No parent stack";
4300
+ super(executor);
4301
+ this[kContext] = {
4302
+ store,
4303
+ stack
4304
+ };
4305
+ }
4306
+ };
4307
+ for (const prop of Object.getOwnPropertyNames(OriginalPromise)) {
4308
+ if (prop !== "prototype" && prop !== "length" && prop !== "name") {
4309
+ if (typeof OriginalPromise[prop] === "function") {
4310
+ global.Promise[prop] = OriginalPromise[prop];
4311
+ }
4312
+ }
4313
+ }
4314
+ }
3452
4315
  const defaults = {
3453
4316
  port: 3e3,
3454
4317
  hostname: "localhost",
3455
4318
  development: process.env.NODE_ENV !== "production",
3456
4319
  enableAsyncLocalStorage: false,
3457
4320
  enableHttpBridge: false,
4321
+ enableOpenApiGen: true,
3458
4322
  reusePort: false
3459
4323
  };
3460
4324
  class Shokupan extends ShokupanRouter {
@@ -3466,8 +4330,11 @@ class Shokupan extends ShokupanRouter {
3466
4330
  composedMiddleware;
3467
4331
  cpuMonitor;
3468
4332
  server;
4333
+ httpServer;
3469
4334
  datastore;
3470
4335
  dbPromise;
4336
+ // Performance: Flattened Router Trie
4337
+ rootTrie;
3471
4338
  get db() {
3472
4339
  return this.datastore;
3473
4340
  }
@@ -3488,11 +4355,32 @@ class Shokupan extends ShokupanRouter {
3488
4355
  line,
3489
4356
  name: "ShokupanApplication"
3490
4357
  };
4358
+ if (this.applicationConfig.defaultSecurityHeaders) {
4359
+ const { SecurityHeaders: SecurityHeaders2 } = require("./plugins/middleware/security-headers");
4360
+ this.use(SecurityHeaders2(this.applicationConfig.defaultSecurityHeaders === true ? {} : this.applicationConfig.defaultSecurityHeaders));
4361
+ }
3491
4362
  if (this.applicationConfig.adapter !== "wintercg") {
3492
4363
  this.dbPromise = this.initDatastore().catch((err) => {
3493
4364
  this.logger?.debug("Failed to initialize default datastore", { error: err });
3494
4365
  });
3495
4366
  }
4367
+ if (this.applicationConfig.enablePromiseMonkeypatch) {
4368
+ enablePromisePatch();
4369
+ const processRef = typeof process !== "undefined" ? process : void 0;
4370
+ if (processRef && processRef.on) {
4371
+ processRef.on("unhandledRejection", (reason, promise) => {
4372
+ const ctx = promise?.[kContext];
4373
+ if (ctx && ctx.store && ctx.store.app === this) {
4374
+ const { requestId } = ctx.store;
4375
+ this.logger.error("Unhandled Rejection in Shokupan Request", {
4376
+ error: reason,
4377
+ requestId,
4378
+ creationStack: ctx.stack
4379
+ });
4380
+ }
4381
+ });
4382
+ }
4383
+ }
3496
4384
  }
3497
4385
  async initDatastore() {
3498
4386
  let engines = this.applicationConfig.surreal?.engines;
@@ -3568,17 +4456,18 @@ class Shokupan extends ShokupanRouter {
3568
4456
  * @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
4457
  * @returns The server instance.
3570
4458
  */
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
- }
4459
+ /**
4460
+ * Prepare the application for listening.
4461
+ * Use this if you want to initialize the app without starting the server immediately.
4462
+ */
4463
+ async start() {
3576
4464
  await Promise.all(this.startupHooks.map((hook) => hook()));
3577
4465
  if (this.applicationConfig.enableOpenApiGen) {
3578
4466
  this.get("/.well-known/openapi.yaml", async (ctx) => {
3579
4467
  try {
3580
4468
  await this.openApiSpecPromise;
3581
- const yaml = jsYaml.dump(this.openApiSpec);
4469
+ const { dump } = await import("js-yaml");
4470
+ const yaml = dump(this.openApiSpec);
3582
4471
  return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
3583
4472
  } catch (e) {
3584
4473
  this.logger?.error("Failed to generate OpenAPI YAML", { error: e });
@@ -3603,13 +4492,13 @@ class Shokupan extends ShokupanRouter {
3603
4492
  auth: config.auth || { type: "none" },
3604
4493
  api: config.api || {
3605
4494
  type: "openapi",
3606
- url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`,
4495
+ url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/.well-known/openapi.yaml`,
3607
4496
  is_user_authenticated: false
3608
4497
  },
3609
- logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/logo.png`,
4498
+ logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/logo.png`,
3610
4499
  // Placeholder default
3611
4500
  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`
4501
+ legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/legal`
3613
4502
  };
3614
4503
  return ctx.json(manifest);
3615
4504
  });
@@ -3622,8 +4511,8 @@ class Shokupan extends ShokupanRouter {
3622
4511
  versions: config.versions || [
3623
4512
  {
3624
4513
  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`
4514
+ url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/`,
4515
+ spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/.well-known/openapi.yaml`
3627
4516
  }
3628
4517
  ]
3629
4518
  };
@@ -3659,28 +4548,20 @@ class Shokupan extends ShokupanRouter {
3659
4548
  await this.asyncApiSpecPromise;
3660
4549
  }
3661
4550
  }
3662
- if (port === 0 && process.platform === "linux") ;
3663
4551
  if (this.applicationConfig.autoBackpressureFeedback === true) {
3664
4552
  this.cpuMonitor = new SystemCpuMonitor();
3665
4553
  this.cpuMonitor.start();
3666
4554
  }
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);
4555
+ }
4556
+ /**
4557
+ * Starts the application server.
4558
+ *
4559
+ * @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.
4560
+ * @returns The server instance.
4561
+ */
4562
+ async listen(port) {
4563
+ this.httpServer = new ShokupanServer(this);
4564
+ this.server = await this.httpServer.listen(port);
3684
4565
  return this.server;
3685
4566
  }
3686
4567
  /**
@@ -3706,10 +4587,14 @@ class Shokupan extends ShokupanRouter {
3706
4587
  this.cpuMonitor.stop();
3707
4588
  this.cpuMonitor = void 0;
3708
4589
  }
3709
- if (this.server) {
4590
+ if (this.httpServer !== void 0) {
4591
+ await this.httpServer.stop(closeActiveConnections);
4592
+ } else if (this.server?.stop) {
3710
4593
  await this.server.stop(closeActiveConnections);
3711
- this.server = void 0;
3712
4594
  }
4595
+ this.server = void 0;
4596
+ const { Container: Container2 } = await Promise.resolve().then(() => di);
4597
+ await Container2.teardown();
3713
4598
  }
3714
4599
  [$dispatch](req) {
3715
4600
  return this.fetch(req);
@@ -3718,6 +4603,9 @@ class Shokupan extends ShokupanRouter {
3718
4603
  * Processes a request by wrapping the standard fetch method.
3719
4604
  */
3720
4605
  async testRequest(options) {
4606
+ if (!this.rootTrie) {
4607
+ this.compile();
4608
+ }
3721
4609
  let url = options.url || options.path || "/";
3722
4610
  if (!url.startsWith("http")) {
3723
4611
  const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
@@ -3770,7 +4658,8 @@ class Shokupan extends ShokupanRouter {
3770
4658
  */
3771
4659
  async fetch(req, server) {
3772
4660
  if (this.applicationConfig.enableTracing) {
3773
- const tracer2 = api.trace.getTracer("shokupan.application");
4661
+ const { trace, context } = await import("@opentelemetry/api");
4662
+ const tracer = trace.getTracer("shokupan.application");
3774
4663
  const store = asyncContext.getStore();
3775
4664
  const attrs = {
3776
4665
  attributes: {
@@ -3779,8 +4668,8 @@ class Shokupan extends ShokupanRouter {
3779
4668
  }
3780
4669
  };
3781
4670
  const parent = store?.span;
3782
- const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
3783
- return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
4671
+ const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
4672
+ return tracer.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
3784
4673
  const ctxStore = new RequestContextStore();
3785
4674
  ctxStore.span = span;
3786
4675
  ctxStore.request = req;
@@ -3788,16 +4677,19 @@ class Shokupan extends ShokupanRouter {
3788
4677
  });
3789
4678
  }
3790
4679
  if (this.applicationConfig.enableAsyncLocalStorage) {
4680
+ const requestId = this.applicationConfig.idGenerator?.() ?? nanoid.nanoid();
3791
4681
  const ctxStore = new RequestContextStore();
3792
4682
  ctxStore.request = req;
3793
- return asyncContext.run(ctxStore, () => this.handleRequest(req, server));
4683
+ ctxStore["requestId"] = requestId;
4684
+ ctxStore["app"] = this;
4685
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server, requestId));
3794
4686
  }
3795
4687
  return this.handleRequest(req, server);
3796
4688
  }
3797
- async handleRequest(req, server) {
4689
+ async handleRequest(req, server, requestId) {
3798
4690
  const request = req;
3799
4691
  const controller = new AbortController();
3800
- const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
4692
+ const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking, requestId);
3801
4693
  const handle = async () => {
3802
4694
  if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
3803
4695
  const msg = "Too Many Requests (CPU Backpressure)";
@@ -3818,9 +4710,91 @@ class Shokupan extends ShokupanRouter {
3818
4710
  ctx[$routeMatched] = true;
3819
4711
  ctx.params = match.params;
3820
4712
  if (bodyParsing) await bodyParsing;
4713
+ if (this.applicationConfig.enableMiddlewareTracking) {
4714
+ const handler = match.handler;
4715
+ const meta = handler.metadata;
4716
+ if (meta) {
4717
+ const trackingStartTime = performance.now();
4718
+ const handlerName = meta.name || handler.name || "anonymous";
4719
+ ctx.handlerStack.push({
4720
+ name: handlerName,
4721
+ file: meta.file,
4722
+ line: meta.line,
4723
+ isBuiltin: meta.isBuiltin,
4724
+ startTime: trackingStartTime,
4725
+ duration: -1
4726
+ });
4727
+ try {
4728
+ const res = await handler(ctx);
4729
+ const duration = performance.now() - trackingStartTime;
4730
+ const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
4731
+ if (stackItem) stackItem.duration = duration;
4732
+ Promise.resolve().then(async () => {
4733
+ try {
4734
+ const db = this.db;
4735
+ if (!db) return;
4736
+ const timestamp = Date.now();
4737
+ await db.upsert(new surrealdb.RecordId("middleware_tracking", {
4738
+ timestamp,
4739
+ name: handlerName
4740
+ }), {
4741
+ name: handlerName,
4742
+ path: ctx.path,
4743
+ timestamp,
4744
+ duration,
4745
+ file: meta.file,
4746
+ line: meta.line,
4747
+ error: void 0,
4748
+ metadata: {
4749
+ isBuiltin: meta.isBuiltin,
4750
+ pluginName: meta.pluginName
4751
+ }
4752
+ });
4753
+ } catch (e) {
4754
+ }
4755
+ });
4756
+ return res;
4757
+ } catch (err) {
4758
+ const duration = performance.now() - trackingStartTime;
4759
+ const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
4760
+ if (stackItem) stackItem.duration = duration;
4761
+ Promise.resolve().then(async () => {
4762
+ try {
4763
+ const db = this.db;
4764
+ if (!db) return;
4765
+ const timestamp = Date.now();
4766
+ await db.upsert(new surrealdb.RecordId("middleware_tracking", {
4767
+ timestamp,
4768
+ name: handlerName
4769
+ }), {
4770
+ name: handlerName,
4771
+ path: ctx.path,
4772
+ timestamp,
4773
+ duration,
4774
+ file: meta.file,
4775
+ line: meta.line,
4776
+ error: String(err),
4777
+ metadata: {
4778
+ isBuiltin: meta.isBuiltin,
4779
+ pluginName: meta.pluginName
4780
+ }
4781
+ });
4782
+ } catch (e) {
4783
+ }
4784
+ });
4785
+ throw err;
4786
+ }
4787
+ }
4788
+ }
3821
4789
  return match.handler(ctx);
3822
4790
  }
3823
- return null;
4791
+ if (ctx.upgrade()) {
4792
+ return void 0;
4793
+ }
4794
+ if (ctx.response.status !== HTTP_STATUS.OK) {
4795
+ return ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
4796
+ }
4797
+ throw new NotFoundError();
3824
4798
  });
3825
4799
  let response;
3826
4800
  if (result instanceof Response) {
@@ -3833,16 +4807,13 @@ class Shokupan extends ShokupanRouter {
3833
4807
  } else if (ctx.isUpgraded) {
3834
4808
  return void 0;
3835
4809
  } else if (ctx[$routeMatched]) {
3836
- response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3837
- } else {
3838
- if (ctx.upgrade()) {
3839
- return void 0;
3840
- }
3841
- if (ctx.response.status !== HTTP_STATUS.OK) {
3842
- response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3843
- } else {
3844
- response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
4810
+ let status = ctx.response.status;
4811
+ if (status === HTTP_STATUS.OK) {
4812
+ status = HTTP_STATUS.NO_CONTENT;
3845
4813
  }
4814
+ response = ctx.send(null, { status, headers: ctx.response.headers });
4815
+ } else {
4816
+ throw new NotFoundError();
3846
4817
  }
3847
4818
  } else if (typeof result === "object") {
3848
4819
  response = ctx.json(result);
@@ -3862,8 +4833,11 @@ class Shokupan extends ShokupanRouter {
3862
4833
  if (err instanceof SyntaxError && err.message.includes("JSON")) {
3863
4834
  status = 400;
3864
4835
  }
3865
- const body = { error: err.message || "Internal Server Error" };
3866
- if (err.errors) body.errors = err.errors;
4836
+ const isDev = this.applicationConfig.development !== false;
4837
+ const message = isDev ? err.message || "Internal Server Error" : "Internal Server Error";
4838
+ const body = { error: message };
4839
+ if (isDev && err.errors) body.errors = err.errors;
4840
+ if (isDev && err.stack) body.stack = err.stack;
3867
4841
  if (this.hasOnErrorHook) await this.runHooks("onError", ctx, err);
3868
4842
  return ctx.json(body, status);
3869
4843
  }
@@ -3892,6 +4866,72 @@ class Shokupan extends ShokupanRouter {
3892
4866
  return res;
3893
4867
  });
3894
4868
  }
4869
+ /**
4870
+ * Compiles all routes into a master Trie for O(1) router lookup.
4871
+ * Use this if adding routes dynamically after start (not recommended but possible).
4872
+ */
4873
+ compile() {
4874
+ this.rootTrie = new RouterTrie();
4875
+ this.flattenRoutes(this.rootTrie, this, "", []);
4876
+ }
4877
+ flattenRoutes(trie, router, prefix, middlewareStack) {
4878
+ let effectiveStack = middlewareStack;
4879
+ if (router !== this) {
4880
+ effectiveStack = [...middlewareStack, ...router.middleware];
4881
+ }
4882
+ const joinPath = (base, segment) => {
4883
+ let b = base;
4884
+ if (b !== "/" && b.endsWith("/")) {
4885
+ b = b.slice(0, -1);
4886
+ }
4887
+ let s = segment;
4888
+ if (s === "/") {
4889
+ return b;
4890
+ }
4891
+ if (s === "") {
4892
+ return b;
4893
+ }
4894
+ if (!s.startsWith("/")) {
4895
+ s = "/" + s;
4896
+ }
4897
+ if (b === "/") {
4898
+ return s;
4899
+ }
4900
+ return b + s;
4901
+ };
4902
+ for (const route of router[$routes]) {
4903
+ const fullPath = joinPath(prefix, route.path);
4904
+ let handler = route.bakedHandler || route.handler;
4905
+ if (effectiveStack.length > 0) {
4906
+ const fn = compose(effectiveStack);
4907
+ const originalHandler = handler;
4908
+ handler = async (ctx) => {
4909
+ return fn(ctx, () => originalHandler(ctx));
4910
+ };
4911
+ handler.originalHandler = originalHandler.originalHandler || originalHandler;
4912
+ }
4913
+ trie.insert(route.method, fullPath, handler);
4914
+ if ((route.path === "/" || route.path === "") && fullPath !== "/") {
4915
+ trie.insert(route.method, fullPath + "/", handler);
4916
+ }
4917
+ }
4918
+ for (const child of router[$childRouters]) {
4919
+ const mountPath = child[$mountPath];
4920
+ const childPrefix = joinPath(prefix, mountPath);
4921
+ this.flattenRoutes(trie, child, childPrefix, effectiveStack);
4922
+ }
4923
+ }
4924
+ find(method, path2) {
4925
+ if (this.rootTrie) {
4926
+ const result = this.rootTrie.search(method, path2);
4927
+ if (result) return result;
4928
+ if (method === "HEAD") {
4929
+ return this.rootTrie.search("GET", path2);
4930
+ }
4931
+ return null;
4932
+ }
4933
+ return super.find(method, path2);
4934
+ }
3895
4935
  }
3896
4936
  function RateLimitMiddleware(options = {}) {
3897
4937
  const windowMs = options.windowMs || 60 * 1e3;
@@ -3901,6 +4941,7 @@ function RateLimitMiddleware(options = {}) {
3901
4941
  const headers = options.headers !== false;
3902
4942
  const mode = options.mode || "user";
3903
4943
  const trustedProxies = options.trustedProxies || [];
4944
+ const cleanupInterval = options.cleanupInterval || windowMs;
3904
4945
  const keyGenerator = options.keyGenerator || ((ctx) => {
3905
4946
  if (mode === "absolute") {
3906
4947
  return "global";
@@ -3930,7 +4971,7 @@ function RateLimitMiddleware(options = {}) {
3930
4971
  hits.delete(key);
3931
4972
  }
3932
4973
  }
3933
- }, windowMs);
4974
+ }, cleanupInterval);
3934
4975
  if (interval.unref) interval.unref();
3935
4976
  const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
3936
4977
  if (skip(ctx)) return next();
@@ -3987,8 +5028,79 @@ function Controller(path2 = "/") {
3987
5028
  target[$controllerPath] = path2;
3988
5029
  };
3989
5030
  }
3990
- function Use(...middleware) {
3991
- return (target, propertyKey, descriptor) => {
5031
+ function Injectable(scope = "singleton") {
5032
+ return (target) => {
5033
+ Reflect.defineMetadata("di:scope", scope, target);
5034
+ };
5035
+ }
5036
+ function Inject(token) {
5037
+ return (target, propertyKey, indexOrDescriptor) => {
5038
+ if (typeof indexOrDescriptor === "undefined" || typeof indexOrDescriptor === "object" && indexOrDescriptor !== null) {
5039
+ const key = String(propertyKey);
5040
+ Object.defineProperty(target, key, {
5041
+ get: () => Container.resolve(token),
5042
+ enumerable: true,
5043
+ configurable: true
5044
+ });
5045
+ return;
5046
+ }
5047
+ if (typeof indexOrDescriptor === "number") {
5048
+ const index = indexOrDescriptor;
5049
+ const existing = Reflect.getMetadata("di:constructor:params", target) || [];
5050
+ existing.push({ index, token });
5051
+ Reflect.defineMetadata("di:constructor:params", existing, target);
5052
+ }
5053
+ };
5054
+ }
5055
+ function Use(tokenOrMiddleware, ...moreMiddleware) {
5056
+ return (target, propertyKey, indexOrDescriptor) => {
5057
+ if (typeof indexOrDescriptor === "number") {
5058
+ const index = indexOrDescriptor;
5059
+ if (!propertyKey) {
5060
+ let token2 = tokenOrMiddleware;
5061
+ if (!token2) {
5062
+ const paramTypes = Reflect.getMetadata("design:paramtypes", target);
5063
+ if (paramTypes && paramTypes[index]) {
5064
+ token2 = paramTypes[index];
5065
+ }
5066
+ }
5067
+ const existing = Reflect.getMetadata("di:constructor:params", target) || [];
5068
+ existing.push({ index, token: token2 });
5069
+ Reflect.defineMetadata("di:constructor:params", existing, target);
5070
+ return;
5071
+ }
5072
+ if (!target[$routeArgs]) target[$routeArgs] = /* @__PURE__ */ new Map();
5073
+ if (!target[$routeArgs].has(propertyKey)) target[$routeArgs].set(propertyKey, []);
5074
+ let token = tokenOrMiddleware;
5075
+ if (!token) {
5076
+ const paramTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey);
5077
+ if (paramTypes && paramTypes[index]) {
5078
+ token = paramTypes[index];
5079
+ }
5080
+ }
5081
+ target[$routeArgs].get(propertyKey).push({
5082
+ index,
5083
+ type: RouteParamType.SERVICE,
5084
+ token
5085
+ });
5086
+ return;
5087
+ }
5088
+ if (typeof propertyKey === "string" && indexOrDescriptor === void 0) {
5089
+ let token = tokenOrMiddleware;
5090
+ if (!token) {
5091
+ token = Reflect.getMetadata("design:type", target, propertyKey);
5092
+ }
5093
+ Object.defineProperty(target, propertyKey, {
5094
+ get: () => {
5095
+ if (!token) throw new Error(`Cannot resolve dependency for ${target.constructor.name}.${propertyKey} - no token provided and types unavailable.`);
5096
+ return Container.resolve(token);
5097
+ },
5098
+ enumerable: true,
5099
+ configurable: true
5100
+ });
5101
+ return;
5102
+ }
5103
+ const middleware = [tokenOrMiddleware, ...moreMiddleware];
3992
5104
  if (!propertyKey) {
3993
5105
  const existing = target[$middleware] || [];
3994
5106
  target[$middleware] = [...existing, ...middleware];
@@ -4068,7 +5180,38 @@ function Event(eventName) {
4068
5180
  function RateLimit(options) {
4069
5181
  return Use(RateLimitMiddleware(options));
4070
5182
  }
4071
- function ApiExplorerApp({ spec, asyncSpec, config }) {
5183
+ function Tool(options) {
5184
+ return (target, propertyKey, descriptor) => {
5185
+ target[$mcpTools] ??= /* @__PURE__ */ new Map();
5186
+ target[$mcpTools].set(propertyKey, {
5187
+ name: options?.name,
5188
+ description: options?.description,
5189
+ inputSchema: options?.inputSchema
5190
+ });
5191
+ };
5192
+ }
5193
+ function Prompt(options) {
5194
+ return (target, propertyKey, descriptor) => {
5195
+ target[$mcpPrompts] ??= /* @__PURE__ */ new Map();
5196
+ target[$mcpPrompts].set(propertyKey, {
5197
+ name: options?.name,
5198
+ description: options?.description,
5199
+ arguments: options?.arguments
5200
+ });
5201
+ };
5202
+ }
5203
+ function Resource(uri, options) {
5204
+ return (target, propertyKey, descriptor) => {
5205
+ target[$mcpResources] ??= /* @__PURE__ */ new Map();
5206
+ target[$mcpResources].set(propertyKey, {
5207
+ uri,
5208
+ name: options?.name,
5209
+ description: options?.description,
5210
+ mimeType: options?.mimeType
5211
+ });
5212
+ };
5213
+ }
5214
+ function ApiExplorerApp({ spec, base, asyncSpec, config }) {
4072
5215
  const hierarchy = /* @__PURE__ */ new Map();
4073
5216
  const addRoute = (groupKey, route) => {
4074
5217
  if (!hierarchy.has(groupKey)) {
@@ -4254,8 +5397,8 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
4254
5397
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
4255
5398
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
4256
5399
  /* @__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" }),
5400
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
5401
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
4259
5402
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
4260
5403
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
4261
5404
  /* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
@@ -4275,7 +5418,7 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
4275
5418
  /* @__PURE__ */ jsxRuntime.jsx(Sidebar$1, { spec, hierarchicalGroups }),
4276
5419
  /* @__PURE__ */ jsxRuntime.jsx(MainContent$1, { allRoutes, config, spec })
4277
5420
  ] }),
4278
- /* @__PURE__ */ jsxRuntime.jsx("script", { src: "explorer-client.mjs", type: "module" })
5421
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/explorer-client.mjs`, type: "module" })
4279
5422
  ] })
4280
5423
  ] });
4281
5424
  }
@@ -4461,8 +5604,14 @@ class ApiExplorerPlugin extends ShokupanRouter {
4461
5604
  this.get("/_source", async (ctx) => {
4462
5605
  const file = ctx.query["file"];
4463
5606
  if (!file) return ctx.text("Missing file parameter", 400);
5607
+ const { resolve, normalize, isAbsolute } = await import("node:path");
5608
+ const cwd = process.cwd();
5609
+ const resolvedPath = resolve(cwd, file);
5610
+ if (!resolvedPath.startsWith(cwd)) {
5611
+ return ctx.text("Forbidden: File must be within project root", 403);
5612
+ }
4464
5613
  try {
4465
- const content = await promises$1.readFile(file, "utf-8");
5614
+ const content = await promises$1.readFile(resolvedPath, "utf-8");
4466
5615
  return ctx.text(content);
4467
5616
  } catch (err) {
4468
5617
  return ctx.text("File not found", 404);
@@ -4475,7 +5624,8 @@ class ApiExplorerPlugin extends ShokupanRouter {
4475
5624
  this.get("/", async (ctx) => {
4476
5625
  const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
4477
5626
  const asyncSpec = ctx.app.asyncApiSpec;
4478
- const element = ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec });
5627
+ const base = this.pluginOptions.path;
5628
+ const element = ApiExplorerApp({ spec: stripSourceCode(spec), base, asyncSpec });
4479
5629
  const html = renderToString(element);
4480
5630
  if (html.length === 0) throw new Error("ApiExplorerPlugin: rendered page is blank.");
4481
5631
  return ctx.html(html);
@@ -4517,12 +5667,12 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
4517
5667
  ] });
4518
5668
  }
4519
5669
  function Sidebar({ navTree, disableSourceView }) {
4520
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar scroller", id: "sidebar", children: [
5670
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar", id: "sidebar", children: [
4521
5671
  /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
4522
5672
  /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "AsyncAPI" }),
4523
5673
  /* @__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
5674
  ] }),
4525
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
5675
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-list scroller", id: "nav-list", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
4526
5676
  ] });
4527
5677
  }
4528
5678
  function NavNode({ node, level, disableSourceView }) {
@@ -4692,7 +5842,7 @@ async function generateAsyncApi(rootRouter, options = {}) {
4692
5842
  let astMiddlewareRegistry = {};
4693
5843
  let applications = [];
4694
5844
  try {
4695
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BAhvpNY_.cjs"));
5845
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BOtveWL-.cjs"));
4696
5846
  const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
4697
5847
  const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
4698
5848
  const analysisResult = await analyzer2.analyze();
@@ -5102,8 +6252,14 @@ class AsyncApiPlugin extends ShokupanRouter {
5102
6252
  if (!file || typeof file !== "string") {
5103
6253
  return ctx.text("Missing file parameter", 400);
5104
6254
  }
6255
+ const { resolve } = await import("node:path");
6256
+ const cwd = process.cwd();
6257
+ const resolvedPath = resolve(cwd, file);
6258
+ if (!resolvedPath.startsWith(cwd)) {
6259
+ return ctx.text("Forbidden: File must be within project root", 403);
6260
+ }
5105
6261
  try {
5106
- const content = await promises.readFile(file, "utf8");
6262
+ const content = await promises.readFile(resolvedPath, "utf8");
5107
6263
  return ctx.text(content);
5108
6264
  } catch (e) {
5109
6265
  return ctx.text("File not found: " + e.message, 404);
@@ -5158,7 +6314,7 @@ class AuthPlugin extends ShokupanRouter {
5158
6314
  }
5159
6315
  }
5160
6316
  async createSession(user, ctx) {
5161
- const alg = "HS256";
6317
+ const alg = this.authConfig.jwtAlgorithm || "HS256";
5162
6318
  const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
5163
6319
  const opts = this.authConfig.cookieOptions || {};
5164
6320
  let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
@@ -5460,7 +6616,7 @@ class ClusterPlugin {
5460
6616
  }
5461
6617
  }
5462
6618
  }
5463
- function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern }) {
6619
+ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern, ignorePaths }) {
5464
6620
  return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
5465
6621
  /* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
5466
6622
  /* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
@@ -5570,22 +6726,22 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
5570
6726
  /* @__PURE__ */ jsxRuntime.jsx("option", { value: "ws", children: "WS" }),
5571
6727
  /* @__PURE__ */ jsxRuntime.jsx("option", { value: "other", children: "Other" })
5572
6728
  ] }),
6729
+ /* @__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: [
6730
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", id: "network-filter-ignore", checked: true }),
6731
+ /* @__PURE__ */ jsxRuntime.jsx("label", { for: "network-filter-ignore", style: "cursor: pointer; font-size: 0.9em; user-select: none;", children: "Excl. Ignored" })
6732
+ ] }),
5573
6733
  /* @__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
6734
  /* @__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
6735
  ] }) }),
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: [
6736
+ /* @__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
6737
  /* @__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: [
6738
+ /* @__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
6739
  /* @__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
6740
  /* @__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" }),
6741
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin: 0; padding: 0", children: "Request Details" }),
5582
6742
  /* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "closeRequestDetails()", style: "background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.2rem;", children: "×" })
5583
6743
  ] }),
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
- ] })
6744
+ /* @__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
6745
  ] })
5590
6746
  ] }) })
5591
6747
  ] }),
@@ -5600,7 +6756,8 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
5600
6756
  const getRequestHeaders = ${getRequestHeadersSource};
5601
6757
  window.SHOKUPAN_CONFIG = {
5602
6758
  rootPath: "${rootPath || ""}",
5603
- linkPattern: "${linkPattern || ""}"
6759
+ linkPattern: "${linkPattern || ""}",
6760
+ ignorePaths: ${JSON.stringify(ignorePaths || [])}
5604
6761
  };
5605
6762
  `
5606
6763
  } }),
@@ -5609,7 +6766,6 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
5609
6766
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/charts.js` }),
5610
6767
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tables.js` }),
5611
6768
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/registry.js` }),
5612
- /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/failures.js` }),
5613
6769
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/requests.js` }),
5614
6770
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tabs.js` })
5615
6771
  ] })
@@ -5678,15 +6834,51 @@ const require$1 = node_module.createRequire(typeof document === "undefined" ? re
5678
6834
  const http = require$1("node:http");
5679
6835
  const https = require$1("node:https");
5680
6836
  class FetchInterceptor {
6837
+ static originalFetch;
6838
+ static originalHttpRequest;
6839
+ static originalHttpsRequest;
5681
6840
  originalFetch;
5682
6841
  originalHttpRequest;
5683
6842
  originalHttpsRequest;
5684
6843
  callbacks = [];
5685
6844
  isPatched = false;
5686
6845
  constructor() {
5687
- this.originalFetch = global.fetch;
5688
- this.originalHttpRequest = http.request;
5689
- this.originalHttpsRequest = https.request;
6846
+ if (!FetchInterceptor.originalFetch) {
6847
+ if (global.fetch.__isPatched) {
6848
+ console.warn("[FetchInterceptor] Global fetch is already patched! Cannot capture original.");
6849
+ } else {
6850
+ FetchInterceptor.originalFetch = global.fetch;
6851
+ FetchInterceptor.originalHttpRequest = http.request;
6852
+ FetchInterceptor.originalHttpsRequest = https.request;
6853
+ }
6854
+ }
6855
+ this.originalFetch = FetchInterceptor.originalFetch || global.fetch;
6856
+ this.originalHttpRequest = FetchInterceptor.originalHttpRequest || http.request;
6857
+ this.originalHttpsRequest = FetchInterceptor.originalHttpsRequest || https.request;
6858
+ }
6859
+ /**
6860
+ * Statically restore the original network methods.
6861
+ * Useful for cleaning up in tests.
6862
+ */
6863
+ /**
6864
+ * Statically restore the original network methods.
6865
+ * Useful for cleaning up in tests.
6866
+ */
6867
+ static restore() {
6868
+ if (FetchInterceptor.originalFetch) {
6869
+ global.fetch = FetchInterceptor.originalFetch;
6870
+ } else if (global.fetch?.__originalFetch) {
6871
+ global.fetch = global.fetch.__originalFetch;
6872
+ } else if (typeof Bun !== "undefined" && Bun.fetch) {
6873
+ global.fetch = Bun.fetch;
6874
+ }
6875
+ if (FetchInterceptor.originalHttpRequest) {
6876
+ http.request = FetchInterceptor.originalHttpRequest;
6877
+ }
6878
+ if (FetchInterceptor.originalHttpsRequest) {
6879
+ https.request = FetchInterceptor.originalHttpsRequest;
6880
+ }
6881
+ console.log("[FetchInterceptor] Network layer restored (static).");
5690
6882
  }
5691
6883
  /**
5692
6884
  * Patches the global `fetch` function to intercept requests.
@@ -5701,37 +6893,33 @@ class FetchInterceptor {
5701
6893
  }
5702
6894
  patchGlobalFetch() {
5703
6895
  const self = this;
6896
+ if (!this.originalFetch && global.fetch.__isPatched && global.fetch.__originalFetch) {
6897
+ this.originalFetch = global.fetch.__originalFetch;
6898
+ }
5704
6899
  const newFetch = async function(input, init) {
5705
6900
  const startTime = performance.now();
5706
6901
  const timestamp = Date.now();
5707
- let method = "GET";
5708
6902
  let url = "";
6903
+ let method = "GET";
5709
6904
  let requestHeaders = {};
5710
- let requestBody = void 0;
5711
6905
  try {
5712
- if (input instanceof node_url.URL) {
5713
- url = input.toString();
5714
- } else if (typeof input === "string") {
6906
+ if (typeof input === "string") {
5715
6907
  url = input;
5716
- } else if (typeof input === "object" && "url" in input) {
6908
+ } else if (input instanceof node_url.URL) {
6909
+ url = input.toString();
6910
+ } else if (input instanceof Request) {
5717
6911
  url = input.url;
5718
6912
  method = input.method;
6913
+ input.headers.forEach((v, k) => requestHeaders[k] = v);
5719
6914
  }
5720
6915
  if (init) {
5721
- if (init.method) method = init.method;
6916
+ if (init.method) method = init.method.toUpperCase();
5722
6917
  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
- }
6918
+ const h = new Headers(init.headers);
6919
+ h.forEach((v, k) => requestHeaders[k] = v);
5730
6920
  }
5731
- if (init.body) requestBody = init.body;
5732
6921
  }
5733
6922
  } catch (e) {
5734
- console.warn("[FetchInterceptor] Failed to parse request arguments", e);
5735
6923
  }
5736
6924
  try {
5737
6925
  const response = await self.originalFetch.apply(global, [input, init]);
@@ -5741,14 +6929,11 @@ class FetchInterceptor {
5741
6929
  method,
5742
6930
  url,
5743
6931
  requestHeaders,
5744
- requestBody,
5745
- status: response.status,
5746
6932
  startTime: timestamp,
5747
6933
  duration,
5748
- ...self.extractRequestMeta(url, requestHeaders),
5749
- protocol: "1.1"
5750
- // native fetch doesn't expose this easily, assume 1.1/2
5751
- });
6934
+ status: response.status,
6935
+ ...self.extractRequestMeta(url, requestHeaders)
6936
+ }).catch((err) => console.error("[FetchInterceptor] Error processing response:", err));
5752
6937
  return response;
5753
6938
  } catch (error) {
5754
6939
  const duration = performance.now() - startTime;
@@ -5756,17 +6941,18 @@ class FetchInterceptor {
5756
6941
  method,
5757
6942
  url,
5758
6943
  requestHeaders,
5759
- requestBody,
5760
6944
  status: 0,
5761
6945
  responseHeaders: {},
5762
- responseBody: `Network Error: ${String(error)}`,
5763
6946
  startTime: timestamp,
5764
- duration
6947
+ duration,
6948
+ responseBody: `Error: ${error.message}`,
6949
+ ...self.extractRequestMeta(url, requestHeaders)
5765
6950
  });
5766
6951
  throw error;
5767
6952
  }
5768
6953
  };
5769
- Object.assign(newFetch, this.originalFetch);
6954
+ newFetch.__isPatched = true;
6955
+ newFetch.__originalFetch = this.originalFetch;
5770
6956
  global.fetch = newFetch;
5771
6957
  }
5772
6958
  patchNodeRequests() {
@@ -6111,6 +7297,9 @@ class Dashboard {
6111
7297
  this.broadcastMetricUpdate(metric);
6112
7298
  };
6113
7299
  this.metricsCollector = new MetricsCollector(this.db, onCollect);
7300
+ if (app.applicationConfig) {
7301
+ app.applicationConfig.enableMiddlewareTracking = true;
7302
+ }
6114
7303
  const fetchInterceptor = new FetchInterceptor();
6115
7304
  fetchInterceptor.patch();
6116
7305
  fetchInterceptor.on((log) => {
@@ -6144,6 +7333,10 @@ class Dashboard {
6144
7333
  responseHeaders: log.responseHeaders
6145
7334
  // No handler stack for outbound
6146
7335
  };
7336
+ const maxLogs = this.dashboardConfig.maxLogEntries ?? 1e3;
7337
+ if (this.metrics.logs.length >= maxLogs) {
7338
+ this.metrics.logs.shift();
7339
+ }
6147
7340
  this.metrics.logs.push(requestData);
6148
7341
  const recordId = new surrealdb.RecordId("request", nanoid.nanoid());
6149
7342
  const idString = recordId.toString();
@@ -6171,17 +7364,9 @@ class Dashboard {
6171
7364
  }
6172
7365
  this.mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
6173
7366
  const hooks = this.getHooks();
6174
- if (!app.middleware) {
6175
- app.middleware = [];
7367
+ if (hooks.onRequestStart) {
7368
+ app.hook("onRequestStart", hooks.onRequestStart);
6176
7369
  }
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
7370
  if (hooks.onResponseEnd) {
6186
7371
  app.hook("onResponseEnd", hooks.onResponseEnd);
6187
7372
  }
@@ -6428,7 +7613,7 @@ class Dashboard {
6428
7613
  if (!this.instrumented && app) {
6429
7614
  this.instrumentApp(app);
6430
7615
  }
6431
- const registry = app?.getComponentRegistry?.();
7616
+ const registry = app?.registry;
6432
7617
  if (registry) {
6433
7618
  this.assignIdsToRegistry(registry, "root");
6434
7619
  }
@@ -6465,31 +7650,56 @@ class Dashboard {
6465
7650
  });
6466
7651
  this.router.post("/replay", async (ctx) => {
6467
7652
  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);
6485
- }
6486
- });
6487
- this.router.get("/**", async (ctx) => {
6488
- const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
6489
- let relativePath = ctx.path;
6490
- if (relativePath.startsWith(mountPath)) {
6491
- relativePath = relativePath.slice(mountPath.length);
6492
- }
7653
+ const direction = body.direction || "inbound";
7654
+ if (direction === "outbound") {
7655
+ const start = performance.now();
7656
+ try {
7657
+ const res = await fetch(body.url, {
7658
+ method: body.method,
7659
+ headers: body.headers,
7660
+ body: body.body ? typeof body.body === "object" ? JSON.stringify(body.body) : body.body : void 0
7661
+ });
7662
+ const text = await res.text();
7663
+ const duration = performance.now() - start;
7664
+ const resHeaders = {};
7665
+ res.headers.forEach((v, k) => resHeaders[k] = v);
7666
+ return ctx.json({
7667
+ status: res.status,
7668
+ statusText: res.statusText,
7669
+ headers: resHeaders,
7670
+ data: text,
7671
+ duration
7672
+ });
7673
+ } catch (e) {
7674
+ return ctx.json({ error: String(e) }, 500);
7675
+ }
7676
+ } else {
7677
+ const app = this[$appRoot];
7678
+ if (!app) return unknownError(ctx);
7679
+ try {
7680
+ const result = await app.internalRequest({
7681
+ method: body.method,
7682
+ path: body.url,
7683
+ // or path
7684
+ headers: body.headers,
7685
+ body: body.body
7686
+ });
7687
+ return ctx.json({
7688
+ status: result.status,
7689
+ headers: result.headers,
7690
+ data: result.body
7691
+ });
7692
+ } catch (e) {
7693
+ return ctx.json({ error: String(e) }, 500);
7694
+ }
7695
+ }
7696
+ });
7697
+ this.router.get("/**", async (ctx) => {
7698
+ const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
7699
+ let relativePath = ctx.path;
7700
+ if (relativePath.startsWith(mountPath)) {
7701
+ relativePath = relativePath.slice(mountPath.length);
7702
+ }
6493
7703
  if (relativePath.startsWith("/")) {
6494
7704
  relativePath = relativePath.slice(1);
6495
7705
  }
@@ -6519,6 +7729,14 @@ class Dashboard {
6519
7729
  const linkPattern = this.getLinkPattern();
6520
7730
  const integrations = this.detectIntegrations();
6521
7731
  const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
7732
+ const ignorePaths = [
7733
+ ...this.dashboardConfig.ignorePaths || [],
7734
+ // Add default ignores for integrations
7735
+ ...Object.values(integrations).filter((p) => !!p).flatMap((p) => {
7736
+ const clean = p.endsWith("/") ? p.slice(0, -1) : p;
7737
+ return [clean, `${clean}/**`];
7738
+ })
7739
+ ];
6522
7740
  const html = renderToString(DashboardApp({
6523
7741
  metrics: this.metrics,
6524
7742
  uptime,
@@ -6526,7 +7744,8 @@ class Dashboard {
6526
7744
  linkPattern,
6527
7745
  integrations,
6528
7746
  base: mountPath,
6529
- getRequestHeadersSource
7747
+ getRequestHeadersSource,
7748
+ ignorePaths
6530
7749
  }));
6531
7750
  return ctx.html(`<!DOCTYPE html>${html}`);
6532
7751
  });
@@ -6673,12 +7892,15 @@ class Dashboard {
6673
7892
  getHooks() {
6674
7893
  return {
6675
7894
  onRequestStart: (ctx) => {
7895
+ if (ctx.path.startsWith(this.mountPath)) return;
6676
7896
  const app = this[$appRoot];
6677
7897
  if (!this.instrumented && app) {
6678
7898
  this.instrumentApp(app);
6679
7899
  }
6680
7900
  this.metrics.totalRequests++;
6681
7901
  this.metrics.activeRequests++;
7902
+ ctx._startTime = performance.now();
7903
+ ctx._reqStartTime = Date.now();
6682
7904
  ctx[$debug] = new Collector(this);
6683
7905
  if (!this.broadcastTimer) {
6684
7906
  this.broadcastTimer = setTimeout(() => {
@@ -6768,7 +7990,7 @@ class Dashboard {
6768
7990
  url: ctx.url.toString(),
6769
7991
  status: response.status,
6770
7992
  duration,
6771
- timestamp: Date.now(),
7993
+ timestamp: ctx._reqStartTime || Date.now() - duration,
6772
7994
  handlerStack: this.serializeHandlerStack(ctx.handlerStack),
6773
7995
  body: this.serializeBody(ctx.responseBody),
6774
7996
  requestBody: ctx.bodyData || ctx.requestBody,
@@ -6789,17 +8011,12 @@ class Dashboard {
6789
8011
  responseHeaders: resHeaders
6790
8012
  };
6791
8013
  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) {
8014
+ this.db.create(new surrealdb.RecordId("request", ctx.requestId), {
8015
+ ...logEntry,
8016
+ direction: "inbound"
8017
+ }).catch((e) => {
6801
8018
  console.error("Failed to record request log", e);
6802
- }
8019
+ });
6803
8020
  const retention = this.dashboardConfig.retentionMs ?? 72e5;
6804
8021
  const cutoff = Date.now() - retention;
6805
8022
  if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
@@ -6897,6 +8114,463 @@ class Dashboard {
6897
8114
  function unknownError(ctx) {
6898
8115
  return ctx.json({ error: "Unknown Error" }, 500);
6899
8116
  }
8117
+ let isPatched = false;
8118
+ function applyMonkeyPatch() {
8119
+ if (isPatched) return;
8120
+ isPatched = true;
8121
+ Error.stackTraceLimit = 50;
8122
+ }
8123
+ async function readSourceContext(filePath, line, contextLines = 5) {
8124
+ if (!filePath || filePath.startsWith("node:") || filePath.startsWith("bun:") || filePath.includes("node_modules")) {
8125
+ return null;
8126
+ }
8127
+ const path2 = filePath.startsWith("file://") ? filePath.slice(7) : filePath;
8128
+ try {
8129
+ const f = bun.file(path2);
8130
+ if (!await f.exists()) return null;
8131
+ const content = await f.text();
8132
+ const allLines = content.split("\n");
8133
+ const targetIndex = line - 1;
8134
+ if (targetIndex < 0 || targetIndex >= allLines.length) return null;
8135
+ const start = Math.max(0, targetIndex - contextLines);
8136
+ const end = Math.min(allLines.length, targetIndex + contextLines + 1);
8137
+ const subset = allLines.slice(start, end).map((code, i) => ({
8138
+ line: start + i + 1,
8139
+ code,
8140
+ isTarget: start + i + 1 === line
8141
+ }));
8142
+ return {
8143
+ lines: subset,
8144
+ startLine: start + 1,
8145
+ file: path2
8146
+ };
8147
+ } catch (e) {
8148
+ return null;
8149
+ }
8150
+ }
8151
+ async function renderErrorView(ctx, error) {
8152
+ const frames = [];
8153
+ const cwd = process.cwd();
8154
+ const errorName = error?.name || "Error";
8155
+ const errorMessage = error?.message || "Unknown error occurred";
8156
+ const errorId = error?.id || ctx.requestId || "unknown-id";
8157
+ const errorTimestamp = error?.timestamp ? new Date(error.timestamp).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
8158
+ const errorScope = error?.scope || {};
8159
+ const lines = (error?.stack || "").split("\n").slice(1);
8160
+ for (const line of lines) {
8161
+ const match = line.match(/at (?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+))\)?/);
8162
+ if (match) {
8163
+ const [_, method, file, lineNo, colNo] = match;
8164
+ const fileName = file || "";
8165
+ let relativeFile = fileName;
8166
+ if (fileName.startsWith(cwd)) {
8167
+ relativeFile = fileName.slice(cwd.length + 1);
8168
+ }
8169
+ let isInternal = fileName.startsWith("node:") || fileName.startsWith("bun:") || fileName === "undefined" || fileName === "";
8170
+ if (isInternal && (method.includes("setTimeout") || method.includes("setInterval") || method.includes("setImmediate"))) {
8171
+ isInternal = false;
8172
+ }
8173
+ let isShokupan = false;
8174
+ if (fileName.includes("node_modules/@dotglitch/shokupan")) {
8175
+ isShokupan = true;
8176
+ } else if (relativeFile.startsWith("src/") || fileName.includes("/shokupan/dist/")) {
8177
+ isShokupan = true;
8178
+ }
8179
+ const isDependency = fileName.includes("node_modules") && !isShokupan;
8180
+ frames.push({
8181
+ method: method || "<anonymous>",
8182
+ file: fileName,
8183
+ line: parseInt(lineNo),
8184
+ column: parseInt(colNo),
8185
+ isNative: false,
8186
+ isInternal,
8187
+ isShokupan,
8188
+ isDependency,
8189
+ shortFile: fileName.split("/").pop() || fileName,
8190
+ relativeFile
8191
+ });
8192
+ }
8193
+ }
8194
+ let focusFrame = frames.find((f) => !f.isInternal && !f.isShokupan && !f.isDependency && !f.isNative);
8195
+ if (!focusFrame) focusFrame = frames[0];
8196
+ let sourceContext = null;
8197
+ if (focusFrame && focusFrame.file && !focusFrame.isInternal) {
8198
+ sourceContext = await readSourceContext(focusFrame.file, focusFrame.line, 8);
8199
+ }
8200
+ const renderFrames = frames.map((frame, index) => {
8201
+ const classes = [
8202
+ "stack-entry",
8203
+ frame.isInternal ? "internal" : "",
8204
+ frame.isShokupan ? "shokupan" : "",
8205
+ frame.isDependency ? "dependency" : "",
8206
+ frame === focusFrame ? "active" : ""
8207
+ ].join(" ");
8208
+ const fileLink = `vscode://file/${frame.file}:${frame.line}:${frame.column}`;
8209
+ return `
8210
+ <li class="${classes}">
8211
+ <a href="${fileLink}" style="text-decoration:none; color:inherit; display:block">
8212
+ <div class="stack-method">${frame.method === "<anonymous>" ? "Anonymous" : frame.method}</div>
8213
+ <div class="stack-file">${frame.relativeFile}:${frame.line}</div>
8214
+ </a>
8215
+ </li>
8216
+ `;
8217
+ }).join("");
8218
+ const highlightCode = (code) => {
8219
+ return code.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/(")(.*?)(")/g, '<span style="color:#a5d6ff">$1$2$3</span>').replace(/(')(.*?)(')/g, '<span style="color:#a5d6ff">$1$2$3</span>').replace(/(`)(.*?)(`)/g, '<span style="color:#a5d6ff">$1$2$3</span>').replace(/\b(const|let|var|function|class|import|export|from|return|if|else|switch|case|default|break|continue|try|catch|finally|throw|new|async|await|interface|type|extends|implements|public|private|protected|static|readonly|true|false|null|undefined)\b/g, '<span style="color:#ff7b72">$1</span>').replace(/(=>|===|==|!=|!==|\|\||&&|\+|\-|\*|\/|%|\+\+|\-\-)/g, '<span style="color:#ff7b72">$1</span>').replace(/\b([A-Z][a-zA-Z0-9_]*)\b/g, '<span style="color:#79c0ff">$1</span>').replace(/\b([a-zA-Z0-9_]+)(?=\()/g, '<span style="color:#d2a8ff">$1</span>').replace(/(\/\/.*)/g, '<span style="color:#8b949e; font-style:italic">$1</span>');
8220
+ };
8221
+ if (sourceContext) {
8222
+ sourceContext.lines.map((l) => `
8223
+ <div class="code-line ${l.isTarget ? "target" : ""}">
8224
+ <div class="line-number">${l.line}</div>
8225
+ <div class="line-content">${highlightCode(l.code)}</div>
8226
+ </div>
8227
+ `).join("");
8228
+ }
8229
+ const renderKV = (data) => {
8230
+ if (!data || Object.keys(data).length === 0) return '<div style="color:var(--text-muted)">None</div>';
8231
+ return `<table class="kv-table">
8232
+ ${Object.entries(data).map(([k, v]) => {
8233
+ let displayVal = String(v);
8234
+ let valClass = "";
8235
+ if (typeof v === "number") {
8236
+ valClass = "kv-val-number";
8237
+ } else if (typeof v === "boolean") {
8238
+ valClass = "kv-val-bool";
8239
+ } else if (typeof v === "object" && v !== null) {
8240
+ try {
8241
+ displayVal = JSON.stringify(v, null, 2);
8242
+ valClass = "kv-val-json";
8243
+ } catch (e) {
8244
+ displayVal = "[Circular]";
8245
+ }
8246
+ }
8247
+ return `
8248
+ <tr>
8249
+ <td class="kv-key">${k}</td>
8250
+ <td class="kv-val ${valClass}">${displayVal}</td>
8251
+ </tr>`;
8252
+ }).join("")}
8253
+ </table>`;
8254
+ };
8255
+ const ICON_COPY = `<svg class="icon" viewBox="0 0 24 24"><path d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z"/></svg>`;
8256
+ return `<!DOCTYPE html>
8257
+ <html lang="en">
8258
+ <head>
8259
+ <meta charset="UTF-8">
8260
+ <title>${errorName}: ${errorMessage}</title>
8261
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css" rel="stylesheet" />
8262
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-highlight/prism-line-highlight.min.css" rel="stylesheet" />
8263
+ <link href="/_shokupan/error-view/prismjs.theme.css" rel="stylesheet" />
8264
+ <link href="/_shokupan/error-view/styles.css" rel="stylesheet" />
8265
+ <link href="/_shokupan/error-view/theme.css" rel="stylesheet" />
8266
+
8267
+ </head>
8268
+ <body class="">
8269
+
8270
+ <div class="page">
8271
+ <!-- HEADER -->
8272
+ <header class="chapter-header">
8273
+ <div class="chapter-meta">
8274
+ <div class="meta-item">
8275
+ <span>${ctx.method}</span>
8276
+ </div>
8277
+ <div class="meta-item">
8278
+ <span>${ctx.url.pathname}</span>
8279
+ </div>
8280
+ <div class="meta-item">
8281
+ <span>${ctx.response.status || 500}</span>
8282
+ </div>
8283
+ <div class="meta-item" style="margin-left:auto">
8284
+ <span class="id-badge" onclick="copyText('${errorId}')" title="Copy ID">ID: ${errorId}</span>
8285
+ </div>
8286
+ </div>
8287
+
8288
+ <h1 class="error-title">${errorName}</h1>
8289
+
8290
+ <div class="error-message-container">
8291
+ <h2 class="error-message">${errorMessage}</h2>
8292
+ <button class="action-btn" onclick="copyText('${errorMessage.replace(/'/g, "\\'")}')" title="Copy Message" style="padding:4px 8px">
8293
+ ${ICON_COPY}
8294
+ </button>
8295
+ </div>
8296
+
8297
+ <div class="actions-bar">
8298
+ <button class="action-btn" onclick="copyText()">
8299
+ ${ICON_COPY} Copy Error
8300
+ </button>
8301
+ <button class="action-btn" onclick="document.getElementById('raw-modal').style.display='flex'">
8302
+ View Raw Error
8303
+ </button>
8304
+ </div>
8305
+ </header>
8306
+
8307
+ <!-- CODE FIGURE -->
8308
+ <section class="figure">
8309
+ <div class="figure-caption">
8310
+ ${focusFrame ? `<a href="vscode://file${focusFrame.file}:${focusFrame.line}" style="color:var(--text-muted); text-decoration:none">${focusFrame ? focusFrame.relativeFile : sourceContext?.file || "Unknown Source"}</a>` : ""}
8311
+ </div>
8312
+ <div class="figure-body">
8313
+ ${sourceContext ? `
8314
+ <pre class="line-numbers" data-line="${sourceContext.lines.find((l) => l.isTarget)?.line}" data-start="${sourceContext.lines[0].line}"><code class="language-typescript">${sourceContext.lines.map((l) => l.code.replace(/</g, "&lt;").replace(/>/g, "&gt;")).join("\n")}</code></pre>
8315
+ ` : `<div style="padding: 2rem; color: var(--text-muted); text-align: center;">Source code not available.</div>`}
8316
+ </div>
8317
+ </section>
8318
+
8319
+ <!-- NARRATIVE STACK -->
8320
+ <section class="narrative">
8321
+ <div class="section-title">
8322
+ <span>Stack Trace</span>
8323
+ <div class="filter-group">
8324
+ <span class="badge" onclick="this.classList.toggle('active'); document.body.classList.toggle('show-internals')">Internals</span>
8325
+ <span class="badge" onclick="this.classList.toggle('active'); document.body.classList.toggle('show-shokupan')">Framework</span>
8326
+ <span class="badge" onclick="this.classList.toggle('active'); document.body.classList.toggle('show-dependencies')">Dependencies</span>
8327
+ </div>
8328
+ </div>
8329
+ <ul class="stack-list">
8330
+ ${renderFrames}
8331
+ </ul>
8332
+ </section>
8333
+
8334
+ <!-- APPENDICES -->
8335
+ <section class="appendix">
8336
+ <div class="section-title">Context & Environment</div>
8337
+ <div class="appendix-grid">
8338
+ <div class="data-block">
8339
+ <h3>Request</h3>
8340
+ ${renderKV({
8341
+ id: errorId,
8342
+ timestamp: errorTimestamp,
8343
+ ...errorScope || {}
8344
+ })}
8345
+ </div>
8346
+ <div class="data-block">
8347
+ <h3>Headers</h3>
8348
+ ${renderKV(Object.fromEntries(ctx.headers))}
8349
+ </div>
8350
+ <div class="data-block">
8351
+ <h3>Query & Params</h3>
8352
+ ${renderKV({ ...ctx.params, ...ctx.query })}
8353
+ </div>
8354
+ </div>
8355
+ </section>
8356
+ </div>
8357
+
8358
+ <!-- RAW ERROR MODAL -->
8359
+ <div id="raw-modal" class="modal-overlay" onclick="if(event.target === this) this.style.display='none'">
8360
+ <div class="modal-content">
8361
+ <div class="modal-header">
8362
+ <span>Raw Error Object</span>
8363
+ <button class="action-btn" onclick="document.getElementById('raw-modal').style.display='none'">Close</button>
8364
+ </div>
8365
+ <div class="modal-body" id="raw-content"></div>
8366
+ </div>
8367
+ </div>
8368
+
8369
+ <!-- PrismJS Scripts -->
8370
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"><\/script>
8371
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"><\/script>
8372
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"><\/script>
8373
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-highlight/prism-line-highlight.min.js"><\/script>
8374
+
8375
+ <script>
8376
+ // Prepare Raw Error
8377
+ // circular ref safe stringify
8378
+ const getCircularReplacer = () => {
8379
+ const seen = new WeakSet();
8380
+ return (key, value) => {
8381
+ if (typeof value === "object" && value !== null) {
8382
+ if (seen.has(value)) {
8383
+ return "[Circular]";
8384
+ }
8385
+ seen.add(value);
8386
+ }
8387
+ return value;
8388
+ };
8389
+ };
8390
+
8391
+ // Inject error data from SERVER side
8392
+ const rawError = ${(() => {
8393
+ const serializeError = (err) => {
8394
+ const obj = {
8395
+ name: err.name,
8396
+ message: err.message,
8397
+ stack: err.stack,
8398
+ ...err
8399
+ // Spread enumerable props
8400
+ };
8401
+ if (err.cause) obj.cause = err.cause;
8402
+ if (err.code) obj.code = err.code;
8403
+ if (err.status) obj.status = err.status;
8404
+ if (err.statusCode) obj.statusCode = err.statusCode;
8405
+ return JSON.stringify(obj, (key, value) => {
8406
+ if (key === "structuredStack") return void 0;
8407
+ return value;
8408
+ }, 2);
8409
+ };
8410
+ return serializeError(error);
8411
+ })()};
8412
+
8413
+ // At this point 'rawError' is an Object in Client JS (because serializeError returned a JSON string)
8414
+ const RAW_ERROR_JSON = JSON.stringify(rawError, getCircularReplacer(), 2);
8415
+ // "Normally printed" usually means standard stacktrace string which includes name/message
8416
+ const RAW_ERROR_TEXT = rawError.stack || (rawError.name + ': ' + rawError.message);
8417
+
8418
+ document.getElementById('raw-content').innerText = RAW_ERROR_JSON;
8419
+
8420
+ function copyText(text) {
8421
+ if (!text) text = RAW_ERROR_TEXT; // Default to text representation (Message + Stack)
8422
+ navigator.clipboard.writeText(text).then(() => {
8423
+ console.log('Copied');
8424
+ });
8425
+ }
8426
+ <\/script>
8427
+ </body>
8428
+ </html>`;
8429
+ }
8430
+ function renderStatusView(ctx, status, error) {
8431
+ const title = `${status} ${error.message || "Error"}`;
8432
+ const method = ctx.method;
8433
+ const path2 = ctx.url.pathname;
8434
+ const css = `
8435
+ body {
8436
+ background: var(--bg-primary);
8437
+ color: var(--text-primary);
8438
+ font-family: var(--shokupan-font);
8439
+ display: flex;
8440
+ align-items: center;
8441
+ justify-content: center;
8442
+ height: 100vh;
8443
+ margin: 0;
8444
+ overflow: hidden;
8445
+ }
8446
+ .container {
8447
+ text-align: center;
8448
+ animation: fadeIn 0.3s ease-out;
8449
+ background: var(--bg-card);
8450
+ padding: 3rem 4rem;
8451
+ border-radius: 16px;
8452
+ border: 1px solid var(--card-border);
8453
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
8454
+ max-width: 600px;
8455
+ }
8456
+ h1 {
8457
+ font-size: 6rem;
8458
+ margin: 0;
8459
+ color: var(--primary);
8460
+ line-height: 1;
8461
+ font-weight: 800;
8462
+ letter-spacing: -2px;
8463
+ text-shadow: 0 4px 20px rgba(255, 179, 128, 0.2);
8464
+ }
8465
+ h2 {
8466
+ font-size: 1.5rem;
8467
+ margin: 1rem 0 2rem 0;
8468
+ font-weight: 400;
8469
+ color: var(--text-secondary);
8470
+ }
8471
+ .meta {
8472
+ color: var(--text-muted);
8473
+ font-family: var(--shokupan-font-mono);
8474
+ font-size: 1rem;
8475
+ background: var(--bg-primary);
8476
+ padding: 0.75rem 1.5rem;
8477
+ border-radius: 8px;
8478
+ display: inline-block;
8479
+ border: 1px solid var(--border-color);
8480
+ }
8481
+ .method {
8482
+ font-weight: bold;
8483
+ margin-right: 0.5rem;
8484
+ padding: 0.2rem 0.5rem;
8485
+ border-radius: 4px;
8486
+ }
8487
+ .path {
8488
+ color: var(--text-primary);
8489
+ }
8490
+ @keyframes fadeIn {
8491
+ from { opacity: 0; transform: translateY(20px); }
8492
+ to { opacity: 1; transform: translateY(0); }
8493
+ }
8494
+ `;
8495
+ return `<!DOCTYPE html>
8496
+ <html lang="en">
8497
+ <head>
8498
+ <meta charset="UTF-8">
8499
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8500
+ <title>${title}</title>
8501
+ <link href="/_shokupan/error-view/theme.css" rel="stylesheet" />
8502
+ <style>${css}</style>
8503
+ </head>
8504
+ <body>
8505
+ <div class="container">
8506
+ <h1>${status}</h1>
8507
+ <h2>${error.message || "An error occurred"}</h2>
8508
+ <div class="meta">
8509
+ <span class="method badge-${method}">${method}</span>
8510
+ <span class="path">${path2}</span>
8511
+ </div>
8512
+ </div>
8513
+ </body>
8514
+ </html>`;
8515
+ }
8516
+ class ErrorView {
8517
+ constructor(config = {}) {
8518
+ this.config = config;
8519
+ }
8520
+ name = "error-view";
8521
+ async onInit(app) {
8522
+ applyMonkeyPatch();
8523
+ const errorViewMiddleware = async (ctx, next) => {
8524
+ try {
8525
+ return await next();
8526
+ } catch (err) {
8527
+ const accept = ctx.get("accept") || "";
8528
+ if (!accept.includes("text/html")) {
8529
+ throw err;
8530
+ }
8531
+ if (!err.timestamp) {
8532
+ Object.defineProperty(err, "timestamp", {
8533
+ value: Date.now(),
8534
+ enumerable: false,
8535
+ writable: true,
8536
+ configurable: true
8537
+ });
8538
+ }
8539
+ if (!err.id) {
8540
+ Object.defineProperty(err, "id", {
8541
+ value: ctx.requestId,
8542
+ enumerable: false,
8543
+ writable: true,
8544
+ configurable: true
8545
+ });
8546
+ }
8547
+ if (!err.scope) {
8548
+ const store = asyncContext.getStore();
8549
+ if (store) {
8550
+ Object.defineProperty(err, "scope", {
8551
+ value: { ...store },
8552
+ enumerable: false,
8553
+ writable: true,
8554
+ configurable: true
8555
+ });
8556
+ }
8557
+ }
8558
+ const status = getErrorStatus(err);
8559
+ if (status === 404 || status === 401 || status === 403) {
8560
+ const html2 = await renderStatusView(ctx, status, err);
8561
+ return ctx.html(html2, status);
8562
+ }
8563
+ const html = await renderErrorView(ctx, err);
8564
+ return ctx.html(html, status);
8565
+ }
8566
+ };
8567
+ Object.defineProperty(errorViewMiddleware, "name", { value: "ErrorViewMiddleware" });
8568
+ const { join } = await import("path");
8569
+ const assetDir = join(void 0, "assets");
8570
+ app.static("/_shokupan/error-view", assetDir);
8571
+ app.use(errorViewMiddleware);
8572
+ }
8573
+ }
6900
8574
  class GraphQLApolloPlugin extends ShokupanRouter {
6901
8575
  // Use generic any or verify type
6902
8576
  constructor(pluginOptions) {
@@ -6971,6 +8645,377 @@ class GraphQLApolloPlugin extends ShokupanRouter {
6971
8645
  });
6972
8646
  }
6973
8647
  }
8648
+ class GraphQLYogaPlugin extends ShokupanRouter {
8649
+ constructor(pluginOptions) {
8650
+ super();
8651
+ this.pluginOptions = pluginOptions;
8652
+ this.pluginOptions.path ??= "/graphql";
8653
+ }
8654
+ yoga;
8655
+ async onInit(app, options) {
8656
+ const { createYoga } = await import("graphql-yoga");
8657
+ const path2 = options?.path || this.pluginOptions.path || "/graphql";
8658
+ this.yoga = createYoga({
8659
+ ...this.pluginOptions.yogaConfig,
8660
+ graphqlEndpoint: path2
8661
+ });
8662
+ app.mount(path2, this);
8663
+ const handler = async (ctx) => {
8664
+ let body;
8665
+ if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") {
8666
+ body = await ctx.body();
8667
+ if (typeof body === "object" && body !== null) {
8668
+ body = JSON.stringify(body);
8669
+ }
8670
+ }
8671
+ const response = await this.yoga.fetch(
8672
+ new Request(ctx.req.url, {
8673
+ method: ctx.req.method,
8674
+ headers: ctx.req.headers,
8675
+ body
8676
+ }),
8677
+ {
8678
+ ...ctx
8679
+ }
8680
+ );
8681
+ response.headers.forEach((value, key) => {
8682
+ ctx.set(key, value);
8683
+ });
8684
+ const text = await response.text();
8685
+ return ctx.send(text, {
8686
+ status: response.status
8687
+ });
8688
+ };
8689
+ this.get("/", handler);
8690
+ this.post("/", handler);
8691
+ this.get("/*", handler);
8692
+ this.post("/*", handler);
8693
+ }
8694
+ }
8695
+ class HtmxPlugin {
8696
+ async onInit(app) {
8697
+ app.use(this.middleware());
8698
+ }
8699
+ middleware() {
8700
+ return async (ctx, next) => {
8701
+ Object.defineProperty(ctx, "isHtmx", {
8702
+ get: () => ctx.req.headers.has("hx-request")
8703
+ });
8704
+ Object.defineProperty(ctx, "isHtmxBoosted", {
8705
+ get: () => ctx.req.headers.has("hx-boosted")
8706
+ });
8707
+ ctx.trigger = (event, options) => {
8708
+ let headerName = "HX-Trigger";
8709
+ if (options?.after === "settle") headerName = "HX-Trigger-After-Settle";
8710
+ if (options?.after === "swap") headerName = "HX-Trigger-After-Swap";
8711
+ let value = JSON.stringify(event);
8712
+ if (typeof event === "string") {
8713
+ value = event;
8714
+ } else {
8715
+ value = JSON.stringify(event);
8716
+ }
8717
+ ctx.set(headerName, value);
8718
+ };
8719
+ ctx.pushUrl = (url) => {
8720
+ ctx.set("HX-Push-Url", url === false ? "false" : url);
8721
+ };
8722
+ ctx.htmxRedirect = (url) => {
8723
+ ctx.set("HX-Redirect", url);
8724
+ };
8725
+ ctx.refresh = () => {
8726
+ ctx.set("HX-Refresh", "true");
8727
+ };
8728
+ return next();
8729
+ };
8730
+ }
8731
+ }
8732
+ function Idempotency(options = {}) {
8733
+ const headerName = options.header || "Idempotency-Key";
8734
+ options.ttl || 24 * 60 * 60 * 1e3;
8735
+ let RecordIdClass;
8736
+ const idempotencyMiddleware = async function IdempotencyMiddleware(ctx, next) {
8737
+ const key = ctx.headers.get(headerName);
8738
+ if (!key) {
8739
+ return next();
8740
+ }
8741
+ try {
8742
+ if (!RecordIdClass) {
8743
+ const mod = await import("surrealdb");
8744
+ RecordIdClass = mod.RecordId;
8745
+ }
8746
+ const stored = await ctx.app.db.select(new RecordIdClass("idempotency", key));
8747
+ if (stored) {
8748
+ const responseHeaders = new Headers(stored.headers);
8749
+ responseHeaders.set("X-Idempotency-Hit", "true");
8750
+ return new Response(stored.body, {
8751
+ status: stored.status,
8752
+ headers: responseHeaders
8753
+ });
8754
+ }
8755
+ } catch (e) {
8756
+ console.error("Idempotency read error:", e);
8757
+ }
8758
+ const result = await next();
8759
+ let response;
8760
+ if (result instanceof Response) {
8761
+ response = result;
8762
+ } else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
8763
+ response = ctx[$finalResponse];
8764
+ } else if (result !== null && result !== void 0) {
8765
+ if (typeof result === "object") {
8766
+ response = await ctx.json(result);
8767
+ } else {
8768
+ response = await ctx.text(String(result));
8769
+ }
8770
+ }
8771
+ if (response instanceof Response) {
8772
+ const clone = response.clone();
8773
+ const bodyText = await clone.text();
8774
+ const headers = {};
8775
+ clone.headers.forEach((v, k) => {
8776
+ headers[k] = v;
8777
+ });
8778
+ const toStore = {
8779
+ status: clone.status,
8780
+ headers,
8781
+ body: bodyText,
8782
+ timestamp: Date.now()
8783
+ };
8784
+ try {
8785
+ await ctx.app.db.upsert(new RecordIdClass("idempotency", key), toStore);
8786
+ } catch (e) {
8787
+ console.error("Idempotency write error:", e);
8788
+ }
8789
+ return response;
8790
+ }
8791
+ return result;
8792
+ };
8793
+ idempotencyMiddleware.isBuiltin = true;
8794
+ idempotencyMiddleware.pluginName = "Idempotency";
8795
+ return idempotencyMiddleware;
8796
+ }
8797
+ class MCPServerPlugin {
8798
+ constructor(options = {}) {
8799
+ this.options = options;
8800
+ options.allowIntrospection ??= true;
8801
+ options.allowToolExecution ??= true;
8802
+ options.path ??= "/mcp";
8803
+ if (!options.path.startsWith("/")) {
8804
+ options.path = "/" + options.path;
8805
+ }
8806
+ options.rootDir ??= process.cwd();
8807
+ }
8808
+ router = new ShokupanRouter();
8809
+ analyzer;
8810
+ onInit(app) {
8811
+ this[$appRoot] = app;
8812
+ this.analyzer = new analyzer_impl.OpenAPIAnalyzer(this.options.rootDir);
8813
+ if (this.options.allowIntrospection) {
8814
+ this.registerTools();
8815
+ this.registerResources();
8816
+ this.registerPrompts();
8817
+ }
8818
+ app.onStart(async () => {
8819
+ app.mount(this.options.path, this.router);
8820
+ this.collectAppMcpItems(app);
8821
+ this.setupRoutes();
8822
+ this.router.metadata = {
8823
+ file: void 0,
8824
+ line: 1,
8825
+ name: "MCPServerPlugin",
8826
+ pluginName: "MCP Server"
8827
+ };
8828
+ });
8829
+ }
8830
+ collectAppMcpItems(app) {
8831
+ const collect = (router) => {
8832
+ if (router.mcpProtocol) {
8833
+ this.router.mcpProtocol.merge(router.mcpProtocol);
8834
+ }
8835
+ router[$childRouters]?.forEach(collect);
8836
+ };
8837
+ collect(app);
8838
+ }
8839
+ setupRoutes() {
8840
+ this.router.get("", (ctx) => {
8841
+ const endpointUrl = `${ctx.protocol}://${ctx.host}${this.options.path}`;
8842
+ const enc = new TextEncoder();
8843
+ return new Response(
8844
+ new ReadableStream({
8845
+ start(controller) {
8846
+ controller.enqueue(enc.encode(`event: endpoint
8847
+ data: ${JSON.stringify(endpointUrl)}
8848
+
8849
+ `));
8850
+ },
8851
+ cancel() {
8852
+ }
8853
+ }),
8854
+ {
8855
+ headers: {
8856
+ "Content-Type": "text/event-stream",
8857
+ "Cache-Control": "no-cache",
8858
+ "Connection": "keep-alive"
8859
+ }
8860
+ }
8861
+ );
8862
+ });
8863
+ this.router.post("", async (ctx) => {
8864
+ let parsedBody;
8865
+ try {
8866
+ parsedBody = await ctx.body();
8867
+ } catch (e) {
8868
+ return ctx.json({
8869
+ jsonrpc: "2.0",
8870
+ id: null,
8871
+ error: { code: -32700, message: "Parse error" }
8872
+ }, 400);
8873
+ }
8874
+ const response = await this.router.mcpProtocol.handleMessage(parsedBody);
8875
+ if (response) {
8876
+ return ctx.json(response);
8877
+ }
8878
+ return ctx.text("", 204);
8879
+ });
8880
+ }
8881
+ registerTools() {
8882
+ const ensureExecutionAllowed = () => {
8883
+ if (!this.options.allowToolExecution) {
8884
+ throw new Error("Tool execution is disabled.");
8885
+ }
8886
+ };
8887
+ this.router.tool(
8888
+ "list_endpoints",
8889
+ {},
8890
+ async () => {
8891
+ ensureExecutionAllowed();
8892
+ const { applications } = await this.analyzer.analyze();
8893
+ const endpoints = applications.flatMap(
8894
+ (app) => app.routes.map((r) => ({
8895
+ method: r.method,
8896
+ path: r.path,
8897
+ handler: r.handlerName,
8898
+ summary: r.summary
8899
+ }))
8900
+ );
8901
+ return {
8902
+ content: [{
8903
+ type: "text",
8904
+ text: JSON.stringify(endpoints, null, 2)
8905
+ }]
8906
+ };
8907
+ }
8908
+ );
8909
+ this.router.tool(
8910
+ "get_endpoint_details",
8911
+ {
8912
+ type: "object",
8913
+ properties: {
8914
+ method: { type: "string" },
8915
+ path: { type: "string" }
8916
+ },
8917
+ required: ["method", "path"]
8918
+ },
8919
+ async ({ method, path: path2 }) => {
8920
+ ensureExecutionAllowed();
8921
+ const { applications } = await this.analyzer.analyze();
8922
+ const route = applications.flatMap((app) => app.routes).find((r) => r.method.toUpperCase() === method.toUpperCase() && r.path === path2);
8923
+ if (!route) {
8924
+ return {
8925
+ content: [{ type: "text", text: `Endpoint ${method} ${path2} not found.` }],
8926
+ isError: true
8927
+ };
8928
+ }
8929
+ return {
8930
+ content: [{
8931
+ type: "text",
8932
+ text: JSON.stringify(route, null, 2)
8933
+ }]
8934
+ };
8935
+ }
8936
+ );
8937
+ }
8938
+ registerResources() {
8939
+ this.router.resource(
8940
+ "mcp://api/openapi.json",
8941
+ {
8942
+ name: "openapi-spec",
8943
+ mimeType: "application/json"
8944
+ },
8945
+ async (uri) => {
8946
+ const { applications } = await this.analyzer.analyze();
8947
+ const endpoints = applications.flatMap(
8948
+ (app) => app.routes.map((r) => ({
8949
+ method: r.method,
8950
+ path: r.path,
8951
+ handler: r.handlerName,
8952
+ summary: r.summary,
8953
+ requestTypes: r.requestTypes,
8954
+ responseType: r.responseType
8955
+ }))
8956
+ );
8957
+ return {
8958
+ contents: [{
8959
+ uri,
8960
+ text: JSON.stringify(endpoints, null, 2)
8961
+ }]
8962
+ };
8963
+ }
8964
+ );
8965
+ this.router.resource(
8966
+ "mcp://api/routes/{method}/{path}/source",
8967
+ {
8968
+ name: "route-source",
8969
+ mimeType: "text/typescript"
8970
+ },
8971
+ async (uri) => {
8972
+ const parts = uri.replace("mcp://", "").split("/");
8973
+ parts[2];
8974
+ throw new Error("Dynamic resource reading not fully implemented in lightweight version yet.");
8975
+ }
8976
+ );
8977
+ }
8978
+ registerPrompts() {
8979
+ this.router.prompt(
8980
+ "generate-client",
8981
+ [
8982
+ { name: "method", required: true },
8983
+ { name: "path", required: true }
8984
+ ],
8985
+ async ({ method, path: path2 }) => {
8986
+ const { applications } = await this.analyzer.analyze();
8987
+ const route = applications.flatMap((app) => app.routes).find((r) => r.method.toUpperCase() === method.toUpperCase() && r.path === path2);
8988
+ if (!route) {
8989
+ return {
8990
+ messages: [{
8991
+ role: "user",
8992
+ content: {
8993
+ type: "text",
8994
+ text: `Start a new task to create a client for ${method} ${path2}. The endpoint was not found in the current analysis.`
8995
+ }
8996
+ }]
8997
+ };
8998
+ }
8999
+ return {
9000
+ messages: [{
9001
+ role: "user",
9002
+ content: {
9003
+ type: "text",
9004
+ text: `Please generate a TypeScript client function for the following endpoint:
9005
+ Method: ${route.method}
9006
+ Path: ${route.path}
9007
+ Summary: ${route.summary || "N/A"}
9008
+ Request Types: ${JSON.stringify(route.requestTypes, null, 2)}
9009
+ Response Type: ${route.responseType || "unknown"}
9010
+
9011
+ Use fetch or axios. Ensure proper typing.`
9012
+ }
9013
+ }]
9014
+ };
9015
+ }
9016
+ );
9017
+ }
9018
+ }
6974
9019
  class ScalarPlugin extends ShokupanRouter {
6975
9020
  constructor(pluginOptions = {}) {
6976
9021
  pluginOptions.config ??= {};
@@ -7159,10 +9204,150 @@ class ScalarPlugin extends ShokupanRouter {
7159
9204
  }
7160
9205
  }
7161
9206
  }
9207
+ function attachSocketIOBridge(io, app) {
9208
+ io.on("connection", (socket) => {
9209
+ socket.onAny(async (event, ...args) => {
9210
+ if (event === "shokupan:request" || event === "http") {
9211
+ return;
9212
+ }
9213
+ const handler = app.findEvent(event);
9214
+ if (handler) {
9215
+ const data = args[0];
9216
+ const req = new ShokupanRequest({
9217
+ url: `socketio://${app.applicationConfig.hostname || "localhost"}/event/${event}`,
9218
+ method: "POST",
9219
+ headers: new Headers({ "content-type": "application/json" }),
9220
+ body: JSON.stringify(data)
9221
+ });
9222
+ const ctx = new ShokupanContext(req, app.server);
9223
+ ctx[$ws] = socket;
9224
+ ctx.io = io;
9225
+ try {
9226
+ for (let i = 0; i < handler.length; i++) {
9227
+ await handler[i](ctx);
9228
+ }
9229
+ } catch (e) {
9230
+ await app.runHooks("onError", ctx, e);
9231
+ if (app.applicationConfig["websocketErrorHandler"]) {
9232
+ await app.applicationConfig["websocketErrorHandler"](e, ctx);
9233
+ } else {
9234
+ console.error(`Error in event ${event}:`, e);
9235
+ }
9236
+ }
9237
+ }
9238
+ });
9239
+ if (app.applicationConfig["enableHttpBridge"]) {
9240
+ socket.on("shokupan:request", async (payload, callback) => {
9241
+ try {
9242
+ const { method, path: path2, headers, body } = payload;
9243
+ const url = new URL(path2, `http://${app.applicationConfig.hostname || "localhost"}:3000`);
9244
+ const req = new Request(url.toString(), {
9245
+ method,
9246
+ headers,
9247
+ body: typeof body === "object" ? JSON.stringify(body) : body
9248
+ });
9249
+ const res = await app.fetch(req);
9250
+ let resBody = await res.text();
9251
+ try {
9252
+ resBody = JSON.parse(resBody);
9253
+ } catch {
9254
+ }
9255
+ const resHeaders = {};
9256
+ res.headers.forEach((v, k) => resHeaders[k] = v);
9257
+ if (typeof callback === "function") {
9258
+ await callback({
9259
+ status: res.status,
9260
+ headers: resHeaders,
9261
+ body: resBody
9262
+ });
9263
+ } else {
9264
+ socket.emit("shokupan:response", {
9265
+ id: payload.id,
9266
+ status: res.status,
9267
+ headers: resHeaders,
9268
+ body: resBody
9269
+ });
9270
+ }
9271
+ } catch (e) {
9272
+ if (typeof callback === "function") {
9273
+ callback({ status: 500, body: { error: e.message } });
9274
+ }
9275
+ }
9276
+ });
9277
+ }
9278
+ });
9279
+ }
9280
+ function createLimitStream(maxSize) {
9281
+ let size = 0;
9282
+ return new TransformStream({
9283
+ transform(chunk, controller) {
9284
+ size += chunk.byteLength || chunk.length;
9285
+ if (size > maxSize) {
9286
+ controller.error(new Error(`Decompressed body size exceeded limit of ${maxSize} bytes`));
9287
+ } else {
9288
+ controller.enqueue(chunk);
9289
+ }
9290
+ }
9291
+ });
9292
+ }
7162
9293
  function Compression(options = {}) {
7163
9294
  const threshold = options.threshold ?? 512;
7164
9295
  const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
9296
+ const decompress = options.decompress ?? true;
9297
+ const maxDecompressedSize = options.maxDecompressedSize ?? 10 * 1024 * 1024;
7165
9298
  const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
9299
+ const requestEncoding = ctx.headers.get("content-encoding");
9300
+ if (decompress && requestEncoding && !ctx.headers.get("content-encoding")?.includes("identity") && ctx.req.body) {
9301
+ let stream = null;
9302
+ if (requestEncoding.includes("br")) {
9303
+ const decompressor = zlib__namespace.createBrotliDecompress();
9304
+ const nodeStream = node_stream.Readable.fromWeb(ctx.req.body);
9305
+ stream = node_stream.Readable.toWeb(nodeStream.pipe(decompressor));
9306
+ } else if (requestEncoding.includes("gzip")) {
9307
+ if (typeof DecompressionStream !== "undefined") {
9308
+ stream = ctx.req.body.pipeThrough(new DecompressionStream("gzip"));
9309
+ } else {
9310
+ const decompressor = zlib__namespace.createGunzip();
9311
+ const nodeStream = node_stream.Readable.fromWeb(ctx.req.body);
9312
+ stream = node_stream.Readable.toWeb(nodeStream.pipe(decompressor));
9313
+ }
9314
+ } else if (requestEncoding.includes("deflate")) {
9315
+ if (typeof DecompressionStream !== "undefined") {
9316
+ stream = ctx.req.body.pipeThrough(new DecompressionStream("deflate"));
9317
+ } else {
9318
+ const decompressor = zlib__namespace.createInflate();
9319
+ const nodeStream = node_stream.Readable.fromWeb(ctx.req.body);
9320
+ stream = node_stream.Readable.toWeb(nodeStream.pipe(decompressor));
9321
+ }
9322
+ }
9323
+ if (stream) {
9324
+ const outputStream = stream.pipeThrough(createLimitStream(maxDecompressedSize));
9325
+ const originalIp = ctx.ip;
9326
+ const originalReq = ctx.req;
9327
+ const newHeaders = new Headers(originalReq.headers);
9328
+ newHeaders.delete("content-encoding");
9329
+ newHeaders.delete("content-length");
9330
+ const newReq = new Proxy(originalReq, {
9331
+ get(target, prop, receiver) {
9332
+ if (prop === "body") return outputStream;
9333
+ if (prop === "headers") return newHeaders;
9334
+ if (prop === "json") return async () => JSON.parse(await new Response(outputStream).text());
9335
+ if (prop === "text") return async () => await new Response(outputStream).text();
9336
+ if (prop === "arrayBuffer") return async () => await new Response(outputStream).arrayBuffer();
9337
+ if (prop === "blob") return async () => await new Response(outputStream).blob();
9338
+ if (prop === "formData") return async () => await new Response(outputStream).formData();
9339
+ return Reflect.get(target, prop, target);
9340
+ }
9341
+ });
9342
+ ctx.request = newReq;
9343
+ if (originalIp) {
9344
+ Object.defineProperty(ctx, "ip", {
9345
+ configurable: true,
9346
+ get: () => originalIp
9347
+ });
9348
+ }
9349
+ }
9350
+ }
7166
9351
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
7167
9352
  let method = null;
7168
9353
  if (acceptEncoding.includes("br")) method = "br";
@@ -7614,7 +9799,7 @@ function openApiValidator() {
7614
9799
  if (validators.body) {
7615
9800
  let body;
7616
9801
  try {
7617
- body = await ctx.req.json().catch(() => ({}));
9802
+ body = await ctx.body();
7618
9803
  } catch {
7619
9804
  body = {};
7620
9805
  }
@@ -7734,10 +9919,171 @@ function enableOpenApiValidation(app) {
7734
9919
  precompileValidators(app, spec);
7735
9920
  });
7736
9921
  }
9922
+ function isPrivateIP(ip) {
9923
+ const ipv4Patterns = [
9924
+ /^10\./,
9925
+ // 10.0.0.0/8
9926
+ /^172\.(1[6-9]|2[0-9]|3[01])\./,
9927
+ // 172.16.0.0/12
9928
+ /^192\.168\./,
9929
+ // 192.168.0.0/16
9930
+ /^127\./,
9931
+ // 127.0.0.0/8 (loopback)
9932
+ /^169\.254\./,
9933
+ // 169.254.0.0/16 (link-local)
9934
+ /^0\.0\.0\.0$/
9935
+ // 0.0.0.0
9936
+ ];
9937
+ const ipv6Patterns = [
9938
+ /^::1$/,
9939
+ // loopback
9940
+ /^fe80:/,
9941
+ // link-local
9942
+ /^fc00:/,
9943
+ // unique local
9944
+ /^fd00:/
9945
+ // unique local
9946
+ ];
9947
+ for (const pattern of ipv4Patterns) {
9948
+ if (pattern.test(ip)) return true;
9949
+ }
9950
+ for (const pattern of ipv6Patterns) {
9951
+ if (pattern.test(ip.toLowerCase())) return true;
9952
+ }
9953
+ return false;
9954
+ }
9955
+ function Proxy$1(options) {
9956
+ const targetUrl = new URL(options.target);
9957
+ if (!["http:", "https:"].includes(targetUrl.protocol)) {
9958
+ throw new Error("Invalid proxy target protocol. Only http and https are allowed.");
9959
+ }
9960
+ if (options.allowedHosts && options.allowedHosts.length > 0) {
9961
+ if (!options.allowedHosts.includes(targetUrl.hostname)) {
9962
+ throw new Error(`Target hostname ${targetUrl.hostname} is not in the allowed hosts list.`);
9963
+ }
9964
+ }
9965
+ if (!options.allowPrivateIPs && isPrivateIP(targetUrl.hostname)) {
9966
+ throw new Error("Proxying to private IP addresses is not allowed.");
9967
+ }
9968
+ return async (ctx, next) => {
9969
+ const req = ctx.request;
9970
+ if (options.ws && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
9971
+ const success = ctx.upgrade({
9972
+ data: {
9973
+ handler: {
9974
+ open: (ws) => handleWSOpen(ws, ctx, options, targetUrl),
9975
+ message: (ws, message) => handleWSMessage(ws, message),
9976
+ close: (ws, code, reason) => handleWSClose(ws, code, reason),
9977
+ drain: (ws) => handleWSDrain()
9978
+ }
9979
+ }
9980
+ });
9981
+ if (success) {
9982
+ return void 0;
9983
+ }
9984
+ }
9985
+ let path2 = ctx.url.pathname;
9986
+ if (options.pathRewrite) {
9987
+ path2 = options.pathRewrite(path2);
9988
+ }
9989
+ const url = new URL(path2 + ctx.url.search, targetUrl);
9990
+ if (!["http:", "https:"].includes(url.protocol)) {
9991
+ return ctx.text("Invalid protocol in proxied URL", 400);
9992
+ }
9993
+ const headers = new Headers(req.headers);
9994
+ if (options.changeOrigin) {
9995
+ headers.set("host", targetUrl.host);
9996
+ }
9997
+ if (options.headers) {
9998
+ Object.entries(options.headers).forEach(([key, value]) => headers.set(key, value));
9999
+ }
10000
+ headers.delete("connection");
10001
+ headers.delete("keep-alive");
10002
+ headers.delete("proxy-authenticate");
10003
+ headers.delete("proxy-authorization");
10004
+ headers.delete("te");
10005
+ headers.delete("trailer");
10006
+ headers.delete("transfer-encoding");
10007
+ headers.delete("upgrade");
10008
+ const proxyReq = new Request(url.toString(), {
10009
+ method: req.method,
10010
+ headers,
10011
+ body: req.body,
10012
+ // @ts-ignore - duplex is needed for some node/bun versions for streaming bodies
10013
+ duplex: "half"
10014
+ });
10015
+ const res = await fetch(proxyReq);
10016
+ return new Response(res.body, {
10017
+ status: res.status,
10018
+ statusText: res.statusText,
10019
+ headers: res.headers
10020
+ });
10021
+ };
10022
+ }
10023
+ const wsMap = /* @__PURE__ */ new WeakMap();
10024
+ function handleWSOpen(ws, ctx, options, targetUrl) {
10025
+ let path2 = ctx.url.pathname;
10026
+ if (options.pathRewrite) {
10027
+ path2 = options.pathRewrite(path2);
10028
+ }
10029
+ const url = new URL(path2 + ctx.url.search, targetUrl);
10030
+ url.protocol = targetUrl.protocol.replace("http", "ws");
10031
+ const headers = {};
10032
+ if (options.changeOrigin) {
10033
+ headers["Host"] = targetUrl.host;
10034
+ }
10035
+ ctx.request.headers.forEach((v, k) => {
10036
+ if (!["upgrade", "connection", "sec-websocket-key", "sec-websocket-version", "sec-websocket-extensions"].includes(k.toLowerCase())) {
10037
+ headers[k] = v;
10038
+ }
10039
+ });
10040
+ const upstream = new WebSocket(url.toString());
10041
+ wsMap.set(ws, upstream);
10042
+ const pendingMessages = [];
10043
+ let isConnected = false;
10044
+ upstream.onopen = () => {
10045
+ isConnected = true;
10046
+ while (pendingMessages.length > 0) {
10047
+ const msg = pendingMessages.shift();
10048
+ upstream.send(msg);
10049
+ }
10050
+ };
10051
+ upstream.onmessage = (event) => {
10052
+ ws.send(event.data);
10053
+ };
10054
+ upstream.onclose = (event) => {
10055
+ ws.close(event.code, event.reason);
10056
+ };
10057
+ upstream.onerror = (err) => {
10058
+ console.error("Upstream WebSocket error:", err);
10059
+ ws.close(1011, "Internal Error");
10060
+ };
10061
+ upstream._pendingRequestMessages = pendingMessages;
10062
+ upstream._isConnected = () => isConnected;
10063
+ }
10064
+ function handleWSMessage(ws, message) {
10065
+ const upstream = wsMap.get(ws);
10066
+ if (!upstream) return;
10067
+ if (upstream._isConnected && upstream._isConnected()) {
10068
+ upstream.send(message);
10069
+ } else {
10070
+ upstream._pendingRequestMessages.push(message);
10071
+ }
10072
+ }
10073
+ function handleWSClose(ws, code, reason) {
10074
+ const upstream = wsMap.get(ws);
10075
+ if (upstream) {
10076
+ if (upstream.readyState === WebSocket.OPEN) {
10077
+ upstream.close(code, reason);
10078
+ }
10079
+ wsMap.delete(ws);
10080
+ }
10081
+ }
10082
+ function handleWSDrain(ws) {
10083
+ }
7737
10084
  function SecurityHeaders(options = {}) {
7738
10085
  const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
7739
- const headers = {};
7740
- const set = (k, v) => headers[k] = v;
10086
+ const set = (k, v) => ctx.response.set(k, v);
7741
10087
  if (options.dnsPrefetchControl !== false) {
7742
10088
  const allow = options.dnsPrefetchControl?.allow;
7743
10089
  set("X-DNS-Prefetch-Control", allow ? "on" : "off");
@@ -7783,14 +10129,6 @@ function SecurityHeaders(options = {}) {
7783
10129
  }
7784
10130
  if (options.hidePoweredBy !== false) ;
7785
10131
  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
10132
  return response;
7795
10133
  };
7796
10134
  securityHeadersMiddleware.isBuiltin = true;
@@ -7955,43 +10293,56 @@ function Session(options) {
7955
10293
  }
7956
10294
  const sessObj = existing;
7957
10295
  Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
7958
- sessObj.save = (cb) => {
7959
- store.set(sessObj.id, sessObj, cb);
10296
+ sessObj.save = () => {
10297
+ return new Promise((resolve, reject) => {
10298
+ store.set(sessObj.id, sessObj, (err) => {
10299
+ if (err) reject(err);
10300
+ else resolve();
10301
+ });
10302
+ });
7960
10303
  };
7961
- sessObj.destroy = (cb) => {
7962
- store.destroy(sessObj.id, (err) => {
7963
- if (cb) cb(err);
10304
+ sessObj.destroy = () => {
10305
+ return new Promise((resolve, reject) => {
10306
+ store.destroy(sessObj.id, (err) => {
10307
+ if (err) reject(err);
10308
+ else resolve();
10309
+ });
7964
10310
  });
7965
10311
  };
7966
- sessObj.regenerate = (cb) => {
7967
- store.destroy(sessObj.id, (err) => {
7968
- sessionID = generateId(ctx);
7969
- const keys = Object.keys(sessObj);
7970
- for (let i = 0; i < keys.length; i++) {
7971
- const key = keys[i];
7972
- if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
7973
- delete sessObj[key];
10312
+ sessObj.regenerate = () => {
10313
+ return new Promise((resolve, reject) => {
10314
+ store.destroy(sessObj.id, (err) => {
10315
+ if (err) return reject(err);
10316
+ sessionID = generateId(ctx);
10317
+ const keys = Object.keys(sessObj);
10318
+ for (let i = 0; i < keys.length; i++) {
10319
+ const key = keys[i];
10320
+ if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
10321
+ delete sessObj[key];
10322
+ }
7974
10323
  }
7975
- }
7976
- Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
7977
- if (cb) cb(err);
10324
+ Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
10325
+ resolve();
10326
+ });
7978
10327
  });
7979
10328
  };
7980
10329
  sessObj.undefined = () => {
7981
10330
  };
7982
- sessObj.reload = (cb) => {
7983
- store.get(sessObj.id, (err, sess2) => {
7984
- if (err) return cb(err);
7985
- if (!sess2) return cb(new Error("Session not found"));
7986
- const keys = Object.keys(sessObj);
7987
- for (let i = 0; i < keys.length; i++) {
7988
- const key = keys[i];
7989
- if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
7990
- delete sessObj[key];
10331
+ sessObj.reload = () => {
10332
+ return new Promise((resolve, reject) => {
10333
+ store.get(sessObj.id, (err, sess2) => {
10334
+ if (err) return reject(err);
10335
+ if (!sess2) return reject(new Error("Session not found"));
10336
+ const keys = Object.keys(sessObj);
10337
+ for (let i = 0; i < keys.length; i++) {
10338
+ const key = keys[i];
10339
+ if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
10340
+ delete sessObj[key];
10341
+ }
7991
10342
  }
7992
- }
7993
- Object.assign(sessObj, sess2);
7994
- cb(null);
10343
+ Object.assign(sessObj, sess2);
10344
+ resolve();
10345
+ });
7995
10346
  });
7996
10347
  };
7997
10348
  sessObj.touch = () => {
@@ -8066,6 +10417,7 @@ exports.$bodyParseError = $bodyParseError;
8066
10417
  exports.$bodyParsed = $bodyParsed;
8067
10418
  exports.$bodyType = $bodyType;
8068
10419
  exports.$cachedBody = $cachedBody;
10420
+ exports.$cachedCookies = $cachedCookies;
8069
10421
  exports.$cachedHost = $cachedHost;
8070
10422
  exports.$cachedHostname = $cachedHostname;
8071
10423
  exports.$cachedOrigin = $cachedOrigin;
@@ -8082,11 +10434,15 @@ exports.$io = $io;
8082
10434
  exports.$isApplication = $isApplication;
8083
10435
  exports.$isMounted = $isMounted;
8084
10436
  exports.$isRouter = $isRouter;
10437
+ exports.$mcpPrompts = $mcpPrompts;
10438
+ exports.$mcpResources = $mcpResources;
10439
+ exports.$mcpTools = $mcpTools;
8085
10440
  exports.$middleware = $middleware;
8086
10441
  exports.$mountPath = $mountPath;
8087
10442
  exports.$parent = $parent;
8088
10443
  exports.$rawBody = $rawBody;
8089
10444
  exports.$requestId = $requestId;
10445
+ exports.$resilienceConfig = $resilienceConfig;
8090
10446
  exports.$routeArgs = $routeArgs;
8091
10447
  exports.$routeMatched = $routeMatched;
8092
10448
  exports.$routeMethods = $routeMethods;
@@ -8108,24 +10464,33 @@ exports.Cors = Cors;
8108
10464
  exports.Ctx = Ctx;
8109
10465
  exports.Dashboard = Dashboard;
8110
10466
  exports.Delete = Delete;
10467
+ exports.ErrorView = ErrorView;
8111
10468
  exports.Event = Event;
8112
10469
  exports.Get = Get;
8113
10470
  exports.GraphQLApolloPlugin = GraphQLApolloPlugin;
10471
+ exports.GraphQLYogaPlugin = GraphQLYogaPlugin;
8114
10472
  exports.HTTPMethods = HTTPMethods;
8115
10473
  exports.Head = Head;
8116
10474
  exports.Headers = Headers$1;
10475
+ exports.HtmxPlugin = HtmxPlugin;
10476
+ exports.Idempotency = Idempotency;
8117
10477
  exports.Inject = Inject;
8118
10478
  exports.Injectable = Injectable;
10479
+ exports.MCPServerPlugin = MCPServerPlugin;
8119
10480
  exports.MemoryStore = MemoryStore;
10481
+ exports.OpenTelemetryPlugin = OpenTelemetryPlugin;
8120
10482
  exports.Options = Options;
8121
10483
  exports.Param = Param;
8122
10484
  exports.Patch = Patch;
8123
10485
  exports.Post = Post;
10486
+ exports.Prompt = Prompt;
10487
+ exports.Proxy = Proxy$1;
8124
10488
  exports.Put = Put;
8125
10489
  exports.Query = Query;
8126
10490
  exports.RateLimit = RateLimit;
8127
10491
  exports.RateLimitMiddleware = RateLimitMiddleware;
8128
10492
  exports.Req = Req;
10493
+ exports.Resource = Resource;
8129
10494
  exports.RouteParamType = RouteParamType;
8130
10495
  exports.RouterRegistry = RouterRegistry;
8131
10496
  exports.ScalarPlugin = ScalarPlugin;
@@ -8138,13 +10503,18 @@ exports.ShokupanRequest = ShokupanRequest;
8138
10503
  exports.ShokupanResponse = ShokupanResponse;
8139
10504
  exports.ShokupanRouter = ShokupanRouter;
8140
10505
  exports.Spec = Spec;
10506
+ exports.Tool = Tool;
8141
10507
  exports.Use = Use;
8142
10508
  exports.ValidationError = ValidationError;
10509
+ exports.attachSocketIOBridge = attachSocketIOBridge;
8143
10510
  exports.compileValidators = compileValidators;
8144
10511
  exports.compose = compose;
8145
10512
  exports.enableOpenApiValidation = enableOpenApiValidation;
8146
10513
  exports.openApiValidator = openApiValidator;
8147
10514
  exports.precompileValidators = precompileValidators;
10515
+ exports.serveStatic = serveStatic;
10516
+ exports.traceHandler = traceHandler;
10517
+ exports.traceMiddleware = traceMiddleware;
8148
10518
  exports.useExpress = useExpress;
8149
10519
  exports.valibot = valibot;
8150
10520
  exports.validate = validate;