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.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { nanoid } from "nanoid";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { inspect } from "node:util";
4
+ import { RecordId, Surreal } from "surrealdb";
4
5
  import { Eta } from "eta";
5
6
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
6
7
  import { resolve, join, sep, basename } from "path";
7
- import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
8
- import { RecordId, Surreal } from "surrealdb";
9
- import { dump } from "js-yaml";
8
+ import { retry, handleAll, ExponentialBackoff, ConstantBackoff, circuitBreaker, ConsecutiveBreaker, timeout, TimeoutStrategy, bulkhead, fallback, wrap } from "cockatiel";
10
9
  import * as http$1 from "node:http";
11
10
  import "node:https";
12
11
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -20,13 +19,124 @@ import * as os from "node:os";
20
19
  import os__default from "node:os";
21
20
  import { createRequire } from "node:module";
22
21
  import { monitorEventLoopDelay } from "node:perf_hooks";
22
+ import { file } from "bun";
23
+ import { OpenAPIAnalyzer } from "./analyzer.impl-DmHe92Oi.js";
23
24
  import { readFileSync } from "node:fs";
24
- import { OpenAPIAnalyzer } from "./analyzer-CnKnQ5KV.js";
25
+ import { OpenAPIAnalyzer as OpenAPIAnalyzer$1 } from "./analyzer-B0fMzeIo.js";
26
+ import { Readable } from "node:stream";
25
27
  import * as zlib from "node:zlib";
26
28
  import Ajv from "ajv";
27
29
  import addFormats from "ajv-formats";
28
30
  import { randomUUID, createHmac } from "crypto";
29
31
  import { EventEmitter } from "events";
32
+ class BodyParser {
33
+ /**
34
+ * Parses the body of a request based on Content-Type header.
35
+ * @param req The ShokupanRequest object
36
+ * @param config Application configuration for limits and parser options
37
+ * @returns The parsed body or throws an error
38
+ */
39
+ static async parse(req, config = {}) {
40
+ const contentType = req.headers.get("content-type") || "";
41
+ const maxBodySize = config.maxBodySize ?? 10 * 1024 * 1024;
42
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
43
+ return {
44
+ type: "json",
45
+ body: await BodyParser.parseJson(req, config.jsonParser || "native", maxBodySize)
46
+ };
47
+ } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
48
+ return {
49
+ type: "formData",
50
+ body: await BodyParser.parseFormData(req, maxBodySize)
51
+ };
52
+ } else {
53
+ return {
54
+ type: "text",
55
+ body: await BodyParser.readRawBody(req, maxBodySize)
56
+ };
57
+ }
58
+ }
59
+ /**
60
+ * Parsing helper for JSON
61
+ */
62
+ static async parseJson(req, parserType, maxBodySize) {
63
+ const rawText = await BodyParser.readRawBody(req, maxBodySize);
64
+ if (parserType === "native") {
65
+ if (!rawText) return {};
66
+ return JSON.parse(rawText);
67
+ } else {
68
+ const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
69
+ const parser = getJSONParser(parserType);
70
+ return parser(rawText);
71
+ }
72
+ }
73
+ /**
74
+ * Parsing helper for FormData
75
+ */
76
+ static async parseFormData(req, maxBodySize) {
77
+ const clHeader = req.headers.get("content-length");
78
+ if (!clHeader) {
79
+ const err = new Error("Length Required");
80
+ err.status = 411;
81
+ throw err;
82
+ }
83
+ const cl = parseInt(clHeader, 10);
84
+ if (isNaN(cl)) {
85
+ const err = new Error("Bad Request");
86
+ err.status = 400;
87
+ throw err;
88
+ }
89
+ if (cl > maxBodySize) {
90
+ const err = new Error("Payload Too Large");
91
+ err.status = 413;
92
+ throw err;
93
+ }
94
+ return req.formData();
95
+ }
96
+ /**
97
+ * Reads raw body as string with size enforcement
98
+ */
99
+ static async readRawBody(req, maxBodySize) {
100
+ if (typeof req.body === "string") {
101
+ const body = req.body;
102
+ if (body.length > maxBodySize) {
103
+ const err = new Error("Payload Too Large");
104
+ err.status = 413;
105
+ throw err;
106
+ }
107
+ return body;
108
+ }
109
+ const reader = req.body?.getReader();
110
+ if (!reader) {
111
+ return "";
112
+ }
113
+ const chunks = [];
114
+ let totalSize = 0;
115
+ try {
116
+ while (true) {
117
+ const { done, value } = await reader.read();
118
+ if (done) break;
119
+ totalSize += value.length;
120
+ if (totalSize > maxBodySize) {
121
+ const err = new Error("Payload Too Large");
122
+ err.status = 413;
123
+ throw err;
124
+ }
125
+ chunks.push(value);
126
+ }
127
+ } finally {
128
+ reader.releaseLock();
129
+ }
130
+ const result = new Uint8Array(totalSize);
131
+ let offset = 0;
132
+ for (let i = 0; i < chunks.length; i++) {
133
+ const chunk = chunks[i];
134
+ result.set(chunk, offset);
135
+ offset += chunk.length;
136
+ }
137
+ return new TextDecoder().decode(result);
138
+ }
139
+ }
30
140
  const HTTP_STATUS = {
31
141
  // 2xx Success
32
142
  OK: 200,
@@ -217,9 +327,14 @@ const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol"
217
327
  const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
218
328
  const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
219
329
  const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
330
+ const $cachedCookies = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedCookies");
220
331
  const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
221
332
  const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
222
333
  const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
334
+ const $mcpTools = /* @__PURE__ */ Symbol.for("Shokupan.mcp.tools");
335
+ const $mcpPrompts = /* @__PURE__ */ Symbol.for("Shokupan.mcp.prompts");
336
+ const $mcpResources = /* @__PURE__ */ Symbol.for("Shokupan.mcp.resources");
337
+ const $resilienceConfig = /* @__PURE__ */ Symbol.for("Shokupan.resilience.config");
223
338
  function isValidCookieDomain(domain, currentHost) {
224
339
  const hostWithoutPort = currentHost.split(":")[0];
225
340
  if (domain === hostWithoutPort) return true;
@@ -275,6 +390,7 @@ class ShokupanContext {
275
390
  [$cachedHost];
276
391
  [$cachedOrigin];
277
392
  [$cachedQuery];
393
+ [$cachedCookies];
278
394
  disconnectCallbacks = [];
279
395
  /**
280
396
  * Registers a callback to be executed when the associated WebSocket disconnects.
@@ -372,13 +488,20 @@ class ShokupanContext {
372
488
  if (this[$cachedQuery]) return this[$cachedQuery];
373
489
  const q = /* @__PURE__ */ Object.create(null);
374
490
  const blocklist = ["__proto__", "constructor", "prototype"];
491
+ const mode = this.app?.applicationConfig?.queryParserMode || "extended";
375
492
  this.url.searchParams.forEach((value, key) => {
376
493
  if (blocklist.includes(key)) return;
377
494
  if (Object.prototype.hasOwnProperty.call(q, key)) {
378
- if (Array.isArray(q[key])) {
379
- q[key].push(value);
495
+ if (mode === "strict") {
496
+ throw new Error(`Duplicate query parameter '${key}' is not allowed in strict mode.`);
497
+ } else if (mode === "simple") {
498
+ q[key] = value;
380
499
  } else {
381
- q[key] = [q[key], value];
500
+ if (Array.isArray(q[key])) {
501
+ q[key].push(value);
502
+ } else {
503
+ q[key] = [q[key], value];
504
+ }
382
505
  }
383
506
  } else {
384
507
  q[key] = value;
@@ -387,6 +510,28 @@ class ShokupanContext {
387
510
  this[$cachedQuery] = q;
388
511
  return q;
389
512
  }
513
+ /**
514
+ * Request cookies
515
+ */
516
+ get cookies() {
517
+ if (this[$cachedCookies]) return this[$cachedCookies];
518
+ const c = /* @__PURE__ */ Object.create(null);
519
+ const cookieHeader = this.request.headers.get("cookie");
520
+ if (cookieHeader) {
521
+ const pairs = cookieHeader.split(";");
522
+ for (let i = 0; i < pairs.length; i++) {
523
+ const pair = pairs[i];
524
+ const index = pair.indexOf("=");
525
+ if (index > 0) {
526
+ const key = pair.slice(0, index).trim();
527
+ const value = pair.slice(index + 1).trim();
528
+ c[key] = decodeURIComponent(value);
529
+ }
530
+ }
531
+ }
532
+ this[$cachedCookies] = c;
533
+ return c;
534
+ }
390
535
  /**
391
536
  * Client IP address
392
537
  */
@@ -561,6 +706,10 @@ class ShokupanContext {
561
706
  }
562
707
  return h;
563
708
  }
709
+ /**
710
+ * Read request body with caching to avoid double parsing.
711
+ * The body is only parsed once and cached for subsequent reads.
712
+ */
564
713
  /**
565
714
  * Read request body with caching to avoid double parsing.
566
715
  * The body is only parsed once and cached for subsequent reads.
@@ -572,29 +721,10 @@ class ShokupanContext {
572
721
  if (this[$bodyParsed] === true) {
573
722
  return this[$cachedBody];
574
723
  }
575
- const contentType = this.request.headers.get("content-type") || "";
576
- if (contentType.includes("application/json") || contentType.includes("+json")) {
577
- const parserType = this.app?.applicationConfig?.jsonParser || "native";
578
- if (parserType === "native") {
579
- try {
580
- this[$cachedBody] = await this.request.json();
581
- } catch (e) {
582
- throw e;
583
- }
584
- } else {
585
- const rawText = await this.request.text();
586
- const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
587
- const parser = getJSONParser(parserType);
588
- this[$cachedBody] = parser(rawText);
589
- }
590
- this[$bodyType] = "json";
591
- } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
592
- this[$cachedBody] = await this.request.formData();
593
- this[$bodyType] = "formData";
594
- } else {
595
- this[$cachedBody] = await this.request.text();
596
- this[$bodyType] = "text";
597
- }
724
+ const config = this.app?.applicationConfig || {};
725
+ const { type, body } = await BodyParser.parse(this.request, config);
726
+ this[$bodyType] = type;
727
+ this[$cachedBody] = body;
598
728
  this[$bodyParsed] = true;
599
729
  return this[$cachedBody];
600
730
  }
@@ -610,45 +740,22 @@ class ShokupanContext {
610
740
  if (this.request.method === "GET" || this.request.method === "HEAD") {
611
741
  return;
612
742
  }
743
+ const maxBodySize = this.app?.applicationConfig?.maxBodySize ?? 10 * 1024 * 1024;
744
+ const contentLength = parseInt(this.request.headers.get("content-length") || "0", 10);
745
+ if (contentLength > maxBodySize) {
746
+ this[$bodyParseError] = new Error("Payload Too Large");
747
+ this[$bodyParseError].status = 413;
748
+ return;
749
+ }
613
750
  try {
614
751
  await this.body();
615
752
  } catch (error) {
616
- this[$bodyParseError] = error;
617
- }
618
- }
619
- /**
620
- * Read raw body from ReadableStream efficiently.
621
- * This is much faster than request.text() for large payloads.
622
- * Also handles the case where body is already a string (e.g., in tests).
623
- */
624
- async readRawBody() {
625
- if (typeof this.request.body === "string") {
626
- return this.request.body;
627
- }
628
- const reader = this.request.body?.getReader();
629
- if (!reader) {
630
- return "";
631
- }
632
- const chunks = [];
633
- let totalSize = 0;
634
- try {
635
- while (true) {
636
- const { done, value } = await reader.read();
637
- if (done) break;
638
- chunks.push(value);
639
- totalSize += value.length;
753
+ if (error.status === 413 || error.message === "Payload Too Large") {
754
+ this[$bodyParseError] = error;
755
+ } else {
756
+ this[$bodyParseError] = error;
640
757
  }
641
- } finally {
642
- reader.releaseLock();
643
758
  }
644
- const result = new Uint8Array(totalSize);
645
- let offset = 0;
646
- for (let i = 0; i < chunks.length; i++) {
647
- const chunk = chunks[i];
648
- result.set(chunk, offset);
649
- offset += chunk.length;
650
- }
651
- return new TextDecoder().decode(result);
652
759
  }
653
760
  /**
654
761
  * Send a response
@@ -748,7 +855,15 @@ class ShokupanContext {
748
855
  }
749
856
  this.response.status = status;
750
857
  const finalHeaders = this.mergeHeaders();
751
- finalHeaders.set("Location", url instanceof Promise ? await url : url);
858
+ const targetUrl = url instanceof Promise ? await url : url;
859
+ if (targetUrl.startsWith("//")) {
860
+ throw new Error("Invalid redirect: Protocol-relative URLs are not allowed.");
861
+ }
862
+ const lowerUrl = targetUrl.toLowerCase();
863
+ if (lowerUrl.startsWith("javascript:") || lowerUrl.startsWith("data:") || lowerUrl.startsWith("vbscript:")) {
864
+ throw new Error(`Invalid redirect: Unsafe protocol '${targetUrl.split(":")[0]}'`);
865
+ }
866
+ finalHeaders.set("Location", targetUrl);
752
867
  this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
753
868
  return this[$finalResponse];
754
869
  }
@@ -806,14 +921,193 @@ class ShokupanContext {
806
921
  const html = await this.renderer(element, args);
807
922
  return this.html(html, status, headers);
808
923
  }
924
+ /**
925
+ * Pipe a ReadableStream to the response
926
+ * @param stream ReadableStream to pipe
927
+ * @param options Response options (status, headers)
928
+ */
929
+ pipe(stream, options) {
930
+ const headers = this.mergeHeaders(options?.headers);
931
+ const status = options?.status ?? this.response.status ?? 200;
932
+ if (this.app?.applicationConfig?.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
933
+ throw new Error(`Invalid HTTP status code: ${status}`);
934
+ }
935
+ this[$finalResponse] = new Response(stream, { status, headers });
936
+ return this[$finalResponse];
937
+ }
938
+ /**
939
+ * Internal helper to create a streaming response with common infrastructure
940
+ * @private
941
+ */
942
+ createStreamHelper(helperFactory, callback, onError, headers) {
943
+ let controller;
944
+ const aborted = { value: false };
945
+ const abortCallbacks = [];
946
+ const encoder = new TextEncoder();
947
+ let helper;
948
+ const stream = new ReadableStream({
949
+ start(ctrl) {
950
+ controller = ctrl;
951
+ helper = helperFactory(controller, aborted, abortCallbacks, encoder);
952
+ (async () => {
953
+ try {
954
+ await callback(helper);
955
+ controller.close();
956
+ } catch (err) {
957
+ if (onError) {
958
+ try {
959
+ await onError(err, helper);
960
+ } catch (handlerErr) {
961
+ console.error("Error in stream error handler:", handlerErr);
962
+ }
963
+ } else {
964
+ console.error("Stream error:", err);
965
+ }
966
+ if (!aborted.value) {
967
+ controller.close();
968
+ }
969
+ }
970
+ })();
971
+ },
972
+ async pull() {
973
+ },
974
+ cancel() {
975
+ aborted.value = true;
976
+ abortCallbacks.forEach((cb) => {
977
+ try {
978
+ cb();
979
+ } catch (err) {
980
+ console.error("Error in abort callback:", err);
981
+ }
982
+ });
983
+ }
984
+ });
985
+ return this.pipe(stream, { headers });
986
+ }
987
+ /**
988
+ * Generic streaming helper for binary/text data
989
+ * @param callback Callback function that receives a StreamHelper
990
+ * @param onError Optional error handler
991
+ */
992
+ stream(callback, onError) {
993
+ return this.createStreamHelper(
994
+ (controller, aborted, abortCallbacks, encoder) => ({
995
+ async write(data) {
996
+ if (aborted.value) return;
997
+ const chunk = typeof data === "string" ? encoder.encode(data) : data;
998
+ controller.enqueue(chunk);
999
+ },
1000
+ async pipe(stream) {
1001
+ if (aborted.value) return;
1002
+ const reader = stream.getReader();
1003
+ try {
1004
+ while (true) {
1005
+ const { done, value } = await reader.read();
1006
+ if (done || aborted.value) break;
1007
+ controller.enqueue(value);
1008
+ }
1009
+ } finally {
1010
+ reader.releaseLock();
1011
+ }
1012
+ },
1013
+ sleep(ms) {
1014
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1015
+ },
1016
+ onAbort(callback2) {
1017
+ abortCallbacks.push(callback2);
1018
+ }
1019
+ }),
1020
+ callback,
1021
+ onError
1022
+ );
1023
+ }
1024
+ /**
1025
+ * Text streaming helper with proper headers
1026
+ * @param callback Callback function that receives a TextStreamHelper
1027
+ * @param onError Optional error handler
1028
+ */
1029
+ streamText(callback, onError) {
1030
+ const headers = new Headers(this.response.headers);
1031
+ headers.set("Content-Type", "text/plain; charset=utf-8");
1032
+ headers.set("Transfer-Encoding", "chunked");
1033
+ headers.set("X-Content-Type-Options", "nosniff");
1034
+ return this.createStreamHelper(
1035
+ (controller, aborted, abortCallbacks, encoder) => ({
1036
+ async write(text) {
1037
+ if (aborted.value) return;
1038
+ controller.enqueue(encoder.encode(text));
1039
+ },
1040
+ async writeln(text) {
1041
+ if (aborted.value) return;
1042
+ controller.enqueue(encoder.encode(text + "\n"));
1043
+ },
1044
+ sleep(ms) {
1045
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1046
+ },
1047
+ onAbort(callback2) {
1048
+ abortCallbacks.push(callback2);
1049
+ }
1050
+ }),
1051
+ callback,
1052
+ onError,
1053
+ headers
1054
+ );
1055
+ }
1056
+ /**
1057
+ * Server-Sent Events (SSE) streaming helper
1058
+ * @param callback Callback function that receives an SSEStreamHelper
1059
+ * @param onError Optional error handler
1060
+ */
1061
+ streamSSE(callback, onError) {
1062
+ const headers = new Headers(this.response.headers);
1063
+ headers.set("Content-Type", "text/event-stream");
1064
+ headers.set("Cache-Control", "no-cache");
1065
+ headers.set("Connection", "keep-alive");
1066
+ return this.createStreamHelper(
1067
+ (controller, aborted, abortCallbacks, encoder) => ({
1068
+ async writeSSE(message) {
1069
+ if (aborted.value) return;
1070
+ let sseMessage = "";
1071
+ if (message.event) {
1072
+ sseMessage += `event: ${message.event}
1073
+ `;
1074
+ }
1075
+ if (message.id !== void 0) {
1076
+ sseMessage += `id: ${message.id}
1077
+ `;
1078
+ }
1079
+ if (message.retry !== void 0) {
1080
+ sseMessage += `retry: ${message.retry}
1081
+ `;
1082
+ }
1083
+ const dataLines = message.data.split("\n");
1084
+ for (const line of dataLines) {
1085
+ sseMessage += `data: ${line}
1086
+ `;
1087
+ }
1088
+ sseMessage += "\n";
1089
+ controller.enqueue(encoder.encode(sseMessage));
1090
+ },
1091
+ sleep(ms) {
1092
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1093
+ },
1094
+ onAbort(callback2) {
1095
+ abortCallbacks.push(callback2);
1096
+ }
1097
+ }),
1098
+ callback,
1099
+ onError,
1100
+ headers
1101
+ );
1102
+ }
809
1103
  }
810
1104
  const compose = (middleware) => {
811
1105
  if (!middleware.length) {
812
- return (context2, next) => {
1106
+ return (context, next) => {
813
1107
  return next ? next() : Promise.resolve();
814
1108
  };
815
1109
  }
816
- return function dispatch(context2, next) {
1110
+ return function dispatch(context, next) {
817
1111
  let index = -1;
818
1112
  async function runner(i) {
819
1113
  if (i <= index) return Promise.reject(new Error("next() called multiple times"));
@@ -822,31 +1116,114 @@ const compose = (middleware) => {
822
1116
  return next ? next() : Promise.resolve();
823
1117
  }
824
1118
  const fn = middleware[i];
825
- if (!context2[$debug]) {
826
- return fn(context2, () => runner(i + 1));
827
- }
828
- const debug = context2[$debug];
829
- const debugId = fn._debugId || fn.name || "anonymous";
830
- const previousNode = debug.getCurrentNode();
831
- debug.trackEdge(previousNode, debugId);
832
- debug.setNode(debugId);
833
- const start = performance.now();
1119
+ if (typeof fn !== "function") {
1120
+ const name = fn?.constructor?.name;
1121
+ console.error(`[Middleware Error] Item at index ${i} is not a function! It is: ${typeof fn} (${name})`, fn);
1122
+ throw new TypeError(`Middleware at index ${i} must be a function, got ${name}`);
1123
+ }
1124
+ const trackingEnabled = context.app?.applicationConfig?.enableMiddlewareTracking;
1125
+ const meta = fn.metadata;
1126
+ let trackingStartTime = 0;
1127
+ if (trackingEnabled && meta) {
1128
+ trackingStartTime = performance.now();
1129
+ context.handlerStack.push({
1130
+ name: meta.name || fn.name || "anonymous",
1131
+ file: meta.file,
1132
+ line: meta.line,
1133
+ isBuiltin: meta.isBuiltin,
1134
+ startTime: trackingStartTime,
1135
+ duration: -1
1136
+ });
1137
+ }
1138
+ const debug = context[$debug];
1139
+ let debugId;
1140
+ let previousNode;
1141
+ let debugStart = 0;
1142
+ if (debug) {
1143
+ debugId = fn._debugId || fn.name || "anonymous";
1144
+ previousNode = debug.getCurrentNode();
1145
+ debug.trackEdge(previousNode, debugId);
1146
+ debug.setNode(debugId);
1147
+ debugStart = performance.now();
1148
+ }
834
1149
  try {
835
- const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
836
- debug.trackStep(debugId, "middleware", performance.now() - start, "success");
1150
+ const res = await fn(context, () => runner(i + 1));
1151
+ if (trackingEnabled && meta) {
1152
+ const duration = performance.now() - trackingStartTime;
1153
+ const stackItem = context.handlerStack[context.handlerStack.length - 1];
1154
+ if (stackItem) stackItem.duration = duration;
1155
+ Promise.resolve().then(async () => {
1156
+ try {
1157
+ const db = context.app?.db;
1158
+ if (!db) return;
1159
+ const timestamp = Date.now();
1160
+ await db.upsert(new RecordId("middleware_tracking", {
1161
+ timestamp,
1162
+ name: meta.name
1163
+ }), {
1164
+ name: meta.name,
1165
+ path: context.path,
1166
+ timestamp,
1167
+ duration,
1168
+ file: meta.file,
1169
+ line: meta.line,
1170
+ error: void 0,
1171
+ metadata: {
1172
+ isBuiltin: meta.isBuiltin,
1173
+ pluginName: meta.pluginName
1174
+ }
1175
+ });
1176
+ } catch (e) {
1177
+ }
1178
+ });
1179
+ }
1180
+ if (debug) {
1181
+ debug.trackStep(debugId, "middleware", performance.now() - debugStart, "success");
1182
+ }
837
1183
  return res;
838
1184
  } catch (err) {
839
- debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
840
- return Promise.reject(err);
1185
+ if (trackingEnabled && meta) {
1186
+ const duration = performance.now() - trackingStartTime;
1187
+ const stackItem = context.handlerStack[context.handlerStack.length - 1];
1188
+ if (stackItem) stackItem.duration = duration;
1189
+ Promise.resolve().then(async () => {
1190
+ try {
1191
+ const db = context.app?.db;
1192
+ if (!db) return;
1193
+ const timestamp = Date.now();
1194
+ await db.upsert(new RecordId("middleware_tracking", {
1195
+ timestamp,
1196
+ name: meta.name
1197
+ }), {
1198
+ name: meta.name,
1199
+ path: context.path,
1200
+ timestamp,
1201
+ duration,
1202
+ file: meta.file,
1203
+ line: meta.line,
1204
+ error: String(err),
1205
+ metadata: {
1206
+ isBuiltin: meta.isBuiltin,
1207
+ pluginName: meta.pluginName
1208
+ }
1209
+ });
1210
+ } catch (e) {
1211
+ }
1212
+ });
1213
+ }
1214
+ if (debug) {
1215
+ debug.trackStep(debugId, "middleware", performance.now() - debugStart, "error", err);
1216
+ }
1217
+ throw err;
841
1218
  } finally {
842
- if (previousNode) debug.setNode(previousNode);
1219
+ if (debug && previousNode) debug.setNode(previousNode);
843
1220
  }
844
1221
  }
845
1222
  return runner(0);
846
1223
  };
847
1224
  };
848
1225
  function isObject(item) {
849
- return item && typeof item === "object" && !Array.isArray(item);
1226
+ return !!(item && typeof item === "object" && !Array.isArray(item));
850
1227
  }
851
1228
  function deepMerge(target, ...sources) {
852
1229
  if (!sources.length) return target;
@@ -1111,7 +1488,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1111
1488
  let astMiddlewareRegistry = {};
1112
1489
  let applications = [];
1113
1490
  try {
1114
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-CnKnQ5KV.js");
1491
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-B0fMzeIo.js");
1115
1492
  const entrypoint = rootRouter.metadata?.file;
1116
1493
  const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
1117
1494
  const analysisResult = await analyzer.analyze();
@@ -1163,7 +1540,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1163
1540
  isBuiltinPlugin = true;
1164
1541
  pluginName = router.metadata.pluginName;
1165
1542
  tag = pluginName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1166
- } else if (router.metadata?.file && router.metadata.file.includes("plugins/application/")) {
1543
+ } else if (router.metadata?.file && router.metadata.file.includes("plugins/application/") && !router.metadata.file.match(/\.(spec|test)\.ts$/)) {
1167
1544
  isBuiltinPlugin = true;
1168
1545
  const match = router.metadata.file.match(/plugins\/application\/([^/]+)/);
1169
1546
  if (match) {
@@ -1321,7 +1698,39 @@ async function generateOpenApi(rootRouter, options = {}) {
1321
1698
  const params = [];
1322
1699
  if (astMatch.requestTypes?.query) {
1323
1700
  for (const [name, _type] of Object.entries(astMatch.requestTypes.query)) {
1324
- params.push({ name, in: "query", schema: { type: "string" } });
1701
+ let type = "string";
1702
+ let format;
1703
+ if (_type === "integer") {
1704
+ type = "integer";
1705
+ format = "int32";
1706
+ } else if (_type === "number") {
1707
+ type = "number";
1708
+ format = "float";
1709
+ } else if (_type === "boolean") type = "boolean";
1710
+ const schema = { type };
1711
+ if (format) schema.format = format;
1712
+ params.push({ name, in: "query", schema });
1713
+ }
1714
+ }
1715
+ if (astMatch.requestTypes?.params) {
1716
+ for (const [name, _type] of Object.entries(astMatch.requestTypes.params)) {
1717
+ let type = "string";
1718
+ let format;
1719
+ if (_type === "integer") {
1720
+ type = "integer";
1721
+ format = "int32";
1722
+ } else if (_type === "number") {
1723
+ type = "number";
1724
+ format = "float";
1725
+ } else if (_type === "boolean") type = "boolean";
1726
+ const schema = { type };
1727
+ if (format) schema.format = format;
1728
+ params.push({ name, in: "path", required: true, schema });
1729
+ }
1730
+ }
1731
+ if (astMatch.requestTypes?.headers) {
1732
+ for (const [name, _type] of Object.entries(astMatch.requestTypes.headers)) {
1733
+ params.push({ name, in: "header", schema: { type: "string" } });
1325
1734
  }
1326
1735
  }
1327
1736
  if (params.length > 0) {
@@ -1337,20 +1746,20 @@ async function generateOpenApi(rootRouter, options = {}) {
1337
1746
  });
1338
1747
  }
1339
1748
  const runtimeSource = (route.handler.originalHandler || route.handler).toString();
1340
- let file;
1749
+ let file2;
1341
1750
  let line;
1342
1751
  if (route.metadata?.file) {
1343
- file = route.metadata.file;
1752
+ file2 = route.metadata.file;
1344
1753
  line = route.metadata.line || 1;
1345
1754
  }
1346
1755
  operation["x-source-info"] = {
1347
1756
  snippet: runtimeSource,
1348
1757
  isRuntime: true,
1349
- ...file ? { file, line: line || 1 } : {}
1758
+ ...file2 ? { file: file2, line: line || 1 } : {}
1350
1759
  };
1351
- if (file) {
1760
+ if (file2) {
1352
1761
  operation["x-shokupan-source"] = {
1353
- file,
1762
+ file: file2,
1354
1763
  line: line || 1,
1355
1764
  code: runtimeSource,
1356
1765
  pluginName: route.handler.pluginName
@@ -1379,9 +1788,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1379
1788
  const mergedParams = [...existingParams];
1380
1789
  pathParams.forEach((p) => {
1381
1790
  const idx = mergedParams.findIndex((ep) => ep.in === "path" && ep.name === p.name);
1382
- if (idx >= 0) {
1383
- mergedParams[idx] = deepMerge(mergedParams[idx], p);
1384
- } else {
1791
+ if (idx === -1) {
1385
1792
  mergedParams.push(p);
1386
1793
  }
1387
1794
  });
@@ -1654,8 +2061,11 @@ function serveStatic(config, prefix) {
1654
2061
  if (typeof Bun !== "undefined") {
1655
2062
  response = new Response(Bun.file(finalPath));
1656
2063
  } else {
1657
- const fileBuffer = await readFile$1(finalPath, { encoding: "binary" });
1658
- response = new Response(fileBuffer);
2064
+ const { createReadStream } = await import("node:fs");
2065
+ const { Readable: Readable2 } = await import("node:stream");
2066
+ const fileStream = createReadStream(finalPath);
2067
+ const webStream = Readable2.toWeb(fileStream);
2068
+ response = new Response(webStream);
1659
2069
  }
1660
2070
  if (config.hooks?.onResponse) {
1661
2071
  const hooked = await config.hooks.onResponse(ctx, response);
@@ -1667,55 +2077,81 @@ function serveStatic(config, prefix) {
1667
2077
  serveStaticMiddleware.pluginName = "ServeStatic";
1668
2078
  return serveStaticMiddleware;
1669
2079
  }
1670
- class Container {
1671
- static services = /* @__PURE__ */ new Map();
1672
- static register(target, instance) {
1673
- this.services.set(target, instance);
1674
- }
1675
- static get(target) {
1676
- return this.services.get(target);
1677
- }
1678
- static has(target) {
1679
- return this.services.has(target);
2080
+ class OpenTelemetryPlugin {
2081
+ constructor(options = {}) {
2082
+ this.options = options;
1680
2083
  }
1681
- static resolve(target) {
1682
- if (this.services.has(target)) {
1683
- return this.services.get(target);
2084
+ api;
2085
+ sdk;
2086
+ async onInit(app) {
2087
+ try {
2088
+ this.api = await import("@opentelemetry/api");
2089
+ } catch (e) {
2090
+ console.warn("OpenTelemetry API not found. OpenTelemetryPlugin will be disabled.");
2091
+ return;
2092
+ }
2093
+ if (this.options.enableAutoInstrumentation !== false) {
2094
+ app.use(this.middleware());
1684
2095
  }
1685
- const instance = new target();
1686
- this.services.set(target, instance);
1687
- return instance;
2096
+ }
2097
+ middleware() {
2098
+ return async (ctx, next) => {
2099
+ if (!this.api) return next();
2100
+ const tracer = this.api.trace.getTracer("shokupan");
2101
+ return tracer.startActiveSpan(`${ctx.req.method} ${ctx.req.path}`, {
2102
+ kind: this.api.SpanKind.SERVER,
2103
+ attributes: {
2104
+ "http.method": ctx.req.method,
2105
+ "http.url": ctx.req.url,
2106
+ "http.host": ctx.req.host,
2107
+ "http.user_agent": ctx.req.headers.get("user-agent") || void 0
2108
+ }
2109
+ }, async (span) => {
2110
+ try {
2111
+ const res = await next();
2112
+ span.setAttributes({
2113
+ "http.status_code": ctx.res.status
2114
+ });
2115
+ if (ctx.res.status >= 500) {
2116
+ span.setStatus({ code: this.api.SpanStatusCode.ERROR });
2117
+ } else {
2118
+ span.setStatus({ code: this.api.SpanStatusCode.OK });
2119
+ }
2120
+ return res;
2121
+ } catch (err) {
2122
+ span.recordException(err);
2123
+ span.setStatus({ code: this.api.SpanStatusCode.ERROR, message: err.message });
2124
+ throw err;
2125
+ } finally {
2126
+ span.end();
2127
+ }
2128
+ });
2129
+ };
1688
2130
  }
1689
2131
  }
1690
- function Injectable() {
1691
- return (target) => {
1692
- };
1693
- }
1694
- function Inject(token) {
1695
- return (target, key) => {
1696
- Object.defineProperty(target, key, {
1697
- get: () => Container.resolve(token),
1698
- enumerable: true,
1699
- configurable: true
1700
- });
1701
- };
1702
- }
1703
- const tracer = trace.getTracer("shokupan.middleware");
1704
- function traceHandler(fn, name) {
1705
- return async function(...args) {
1706
- return tracer.startActiveSpan(`route handler - ${name}`, {
1707
- kind: SpanKind.INTERNAL,
2132
+ function traceMiddleware(fn, name) {
2133
+ let api;
2134
+ try {
2135
+ api = require("@opentelemetry/api");
2136
+ } catch {
2137
+ }
2138
+ if (!api) return fn;
2139
+ const tracer = api.trace.getTracer("shokupan.middleware");
2140
+ const middlewareName = name || fn.name || "anonymous middleware";
2141
+ return async (ctx, next) => {
2142
+ return tracer.startActiveSpan(`middleware - ${middlewareName}`, {
2143
+ kind: api.SpanKind.INTERNAL,
1708
2144
  attributes: {
1709
- "http.route": name,
1710
- "component": "shokupan.route"
2145
+ "code.function": middlewareName,
2146
+ "component": "shokupan.middleware"
1711
2147
  }
1712
2148
  }, async (span) => {
1713
2149
  try {
1714
- const result = await fn.apply(this, args);
2150
+ const result = await fn(ctx, next);
1715
2151
  return result;
1716
2152
  } catch (err) {
1717
2153
  span.recordException(err);
1718
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
2154
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
1719
2155
  throw err;
1720
2156
  } finally {
1721
2157
  span.end();
@@ -1723,9 +2159,183 @@ function traceHandler(fn, name) {
1723
2159
  });
1724
2160
  };
1725
2161
  }
1726
- function getCallerInfo(skipFrames = 1) {
1727
- let file = "unknown";
1728
- let line = 0;
2162
+ function traceHandler(fn, name) {
2163
+ let api;
2164
+ try {
2165
+ api = require("@opentelemetry/api");
2166
+ } catch {
2167
+ }
2168
+ if (!api) return fn;
2169
+ const tracer = api.trace.getTracer("shokupan.middleware");
2170
+ return async function(...args) {
2171
+ return tracer.startActiveSpan(`route handler - ${name}`, {
2172
+ kind: api.SpanKind.INTERNAL,
2173
+ attributes: {
2174
+ "http.route": name,
2175
+ "component": "shokupan.route"
2176
+ }
2177
+ }, async (span) => {
2178
+ try {
2179
+ const result = await fn.apply(this, args);
2180
+ return result;
2181
+ } catch (err) {
2182
+ span.recordException(err);
2183
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
2184
+ throw err;
2185
+ } finally {
2186
+ span.end();
2187
+ }
2188
+ });
2189
+ };
2190
+ }
2191
+ class ResilienceFactory {
2192
+ static createPolicy(config) {
2193
+ const policies = [];
2194
+ if (config.retry) {
2195
+ const builder = handleAll;
2196
+ let retries = (config.retry.attempts ?? 3) - 1;
2197
+ if (retries < 0) retries = 0;
2198
+ let retryPolicy;
2199
+ if (config.retry.backoff === "exponential") {
2200
+ retryPolicy = retry(builder, {
2201
+ maxAttempts: retries,
2202
+ backoff: new ExponentialBackoff({
2203
+ initialDelay: config.retry.delay || 1e3,
2204
+ maxDelay: config.retry.maxDelay || 3e4
2205
+ })
2206
+ });
2207
+ } else {
2208
+ retryPolicy = retry(builder, {
2209
+ maxAttempts: retries,
2210
+ backoff: new ConstantBackoff(config.retry.delay || 1e3)
2211
+ });
2212
+ }
2213
+ policies.push(retryPolicy);
2214
+ }
2215
+ if (config.circuitBreaker) {
2216
+ const builder = handleAll;
2217
+ const breaker = circuitBreaker(builder, {
2218
+ halfOpenAfter: config.circuitBreaker.resetTimeout || 1e4,
2219
+ breaker: new ConsecutiveBreaker(config.circuitBreaker.threshold || 5)
2220
+ });
2221
+ policies.push(breaker);
2222
+ }
2223
+ if (config.timeout) {
2224
+ policies.push(timeout(config.timeout, { strategy: TimeoutStrategy.Aggressive, abortOnReturn: true }));
2225
+ }
2226
+ if (config.bulkhead) {
2227
+ policies.push(bulkhead(config.bulkhead));
2228
+ }
2229
+ if (config.fallback !== void 0) {
2230
+ const builder = handleAll;
2231
+ const fb = fallback(builder, config.fallback);
2232
+ policies.push(fb);
2233
+ }
2234
+ if (policies.length === 0) {
2235
+ return { execute: (fn) => fn() };
2236
+ }
2237
+ return wrap(...policies.reverse());
2238
+ }
2239
+ }
2240
+ const metadataStore = /* @__PURE__ */ new WeakMap();
2241
+ function defineMetadata(key, value, target, propertyKey) {
2242
+ let targetMetadata = metadataStore.get(target);
2243
+ if (!targetMetadata) {
2244
+ targetMetadata = /* @__PURE__ */ new Map();
2245
+ metadataStore.set(target, targetMetadata);
2246
+ }
2247
+ const storageKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : key;
2248
+ targetMetadata.set(storageKey, value);
2249
+ }
2250
+ function getMetadata(key, target, propertyKey) {
2251
+ const targetMetadata = metadataStore.get(target);
2252
+ if (!targetMetadata) return void 0;
2253
+ const storageKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : key;
2254
+ return targetMetadata.get(storageKey);
2255
+ }
2256
+ if (typeof Reflect === "object") {
2257
+ if (!Reflect.defineMetadata) {
2258
+ Reflect.defineMetadata = defineMetadata;
2259
+ }
2260
+ if (!Reflect.getMetadata) {
2261
+ Reflect.getMetadata = getMetadata;
2262
+ }
2263
+ if (!Reflect.metadata) {
2264
+ Reflect.metadata = function(metadataKey, metadataValue) {
2265
+ return function decorator(target, propertyKey) {
2266
+ defineMetadata(metadataKey, metadataValue, target, propertyKey);
2267
+ };
2268
+ };
2269
+ }
2270
+ }
2271
+ class Container {
2272
+ static services = /* @__PURE__ */ new Map();
2273
+ static register(target, instance) {
2274
+ this.services.set(target, instance);
2275
+ }
2276
+ static get(target) {
2277
+ return this.services.get(target);
2278
+ }
2279
+ static has(target) {
2280
+ return this.services.has(target);
2281
+ }
2282
+ static cache = /* @__PURE__ */ new Map();
2283
+ static resolvingStack = /* @__PURE__ */ new Set();
2284
+ static resolve(target) {
2285
+ if (this.services.has(target)) {
2286
+ return this.services.get(target);
2287
+ }
2288
+ if (this.resolvingStack.has(target)) {
2289
+ const cycle = Array.from(this.resolvingStack);
2290
+ cycle.push(target);
2291
+ throw new Error(`Circular dependency detected: ${cycle.map((t) => t.name || t).join(" -> ")}`);
2292
+ }
2293
+ this.resolvingStack.add(target);
2294
+ try {
2295
+ let meta = this.cache.get(target);
2296
+ if (!meta) {
2297
+ const scope = Reflect.getMetadata("di:scope", target) || "singleton";
2298
+ const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
2299
+ const manualTokens = Reflect.getMetadata("di:constructor:params", target) || [];
2300
+ const dependencies = paramTypes.map((param, index) => {
2301
+ const manual = manualTokens.find((t) => t.index === index);
2302
+ if (manual && manual.token) return manual.token;
2303
+ if (param === String || param === Number || param === Boolean || param === Object || param === void 0) return void 0;
2304
+ return param;
2305
+ });
2306
+ meta = { scope, dependencies };
2307
+ this.cache.set(target, meta);
2308
+ }
2309
+ const args = meta.dependencies.map((dep) => dep ? Container.resolve(dep) : void 0);
2310
+ const instance = new target(...args);
2311
+ if (typeof instance.onInit === "function") {
2312
+ instance.onInit();
2313
+ }
2314
+ if (meta.scope === "singleton") {
2315
+ this.services.set(target, instance);
2316
+ }
2317
+ return instance;
2318
+ } finally {
2319
+ this.resolvingStack.delete(target);
2320
+ }
2321
+ }
2322
+ static async teardown() {
2323
+ for (const [target, instance] of this.services.entries()) {
2324
+ if (typeof instance.onDestroy === "function") {
2325
+ await instance.onDestroy();
2326
+ }
2327
+ }
2328
+ this.services.clear();
2329
+ this.cache.clear();
2330
+ }
2331
+ }
2332
+ const di = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2333
+ __proto__: null,
2334
+ Container
2335
+ }, Symbol.toStringTag, { value: "Module" }));
2336
+ function getCallerInfo(skipFrames = 1) {
2337
+ let file2 = "unknown";
2338
+ let line = 0;
1729
2339
  try {
1730
2340
  const err = new Error();
1731
2341
  const stack = err.stack?.split("\n") || [];
@@ -1740,19 +2350,20 @@ function getCallerInfo(skipFrames = 1) {
1740
2350
  if (l.includes("src/router.ts")) continue;
1741
2351
  if (l.includes("src/util/decorators.ts")) continue;
1742
2352
  if (l.includes("src/shokupan.ts")) continue;
2353
+ if (l.includes("src/plugins/application/openapi/openapi.ts")) continue;
1743
2354
  found++;
1744
2355
  if (found >= skipFrames) {
1745
2356
  const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1746
2357
  if (match) {
1747
- file = match[1];
2358
+ file2 = match[1];
1748
2359
  line = parseInt(match[2], 10);
1749
- return { file, line };
2360
+ return { file: file2, line };
1750
2361
  }
1751
2362
  }
1752
2363
  }
1753
2364
  } catch (e) {
1754
2365
  }
1755
- return { file, line };
2366
+ return { file: file2, line };
1756
2367
  }
1757
2368
  const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1758
2369
  var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
@@ -1762,6 +2373,7 @@ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1762
2373
  RouteParamType2["HEADER"] = "HEADER";
1763
2374
  RouteParamType2["REQUEST"] = "REQUEST";
1764
2375
  RouteParamType2["CONTEXT"] = "CONTEXT";
2376
+ RouteParamType2["SERVICE"] = "SERVICE";
1765
2377
  return RouteParamType2;
1766
2378
  })(RouteParamType || {});
1767
2379
  class ControllerScanner {
@@ -1793,7 +2405,7 @@ class ControllerScanner {
1793
2405
  line: info.line,
1794
2406
  name: instance.constructor.name
1795
2407
  };
1796
- router.registerControllerInstance(instance);
2408
+ router.bindController(instance);
1797
2409
  const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1798
2410
  const proto = Object.getPrototypeOf(instance);
1799
2411
  const methods = /* @__PURE__ */ new Set();
@@ -1807,6 +2419,10 @@ class ControllerScanner {
1807
2419
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1808
2420
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1809
2421
  const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
2422
+ const mcpTools = instance[$mcpTools] || proto && proto[$mcpTools];
2423
+ const mcpPrompts = instance[$mcpPrompts] || proto && proto[$mcpPrompts];
2424
+ const mcpResources = instance[$mcpResources] || proto && proto[$mcpResources];
2425
+ const resilienceConfigMap = instance[$resilienceConfig] || proto && proto[$resilienceConfig];
1810
2426
  let routesAttached = 0;
1811
2427
  for (let i = 0; i < Array.from(methods).length; i++) {
1812
2428
  const name = Array.from(methods)[i];
@@ -1915,6 +2531,9 @@ class ControllerScanner {
1915
2531
  case RouteParamType.CONTEXT:
1916
2532
  args[arg.index] = ctx;
1917
2533
  break;
2534
+ case RouteParamType.SERVICE:
2535
+ args[arg.index] = Container.resolve(arg.token);
2536
+ break;
1918
2537
  }
1919
2538
  }
1920
2539
  }
@@ -1928,6 +2547,14 @@ class ControllerScanner {
1928
2547
  return composed(ctx, () => wrappedHandler(ctx));
1929
2548
  };
1930
2549
  }
2550
+ const config = resilienceConfigMap?.get(name);
2551
+ if (config) {
2552
+ const policy = ResilienceFactory.createPolicy(config);
2553
+ const baseHandler = finalHandler;
2554
+ finalHandler = async (ctx) => {
2555
+ return policy.execute(() => baseHandler(ctx));
2556
+ };
2557
+ }
1931
2558
  finalHandler.originalHandler = originalHandler;
1932
2559
  if (finalHandler !== wrappedHandler) {
1933
2560
  wrappedHandler.originalHandler = originalHandler;
@@ -1984,6 +2611,25 @@ class ControllerScanner {
1984
2611
  wrappedHandler.originalHandler = originalHandler;
1985
2612
  router.event(eventConfig.eventName, wrappedHandler);
1986
2613
  }
2614
+ const toolConfig = mcpTools?.get(name);
2615
+ if (toolConfig) {
2616
+ const handler = originalHandler.bind(instance);
2617
+ router.tool(toolConfig.name || name, toolConfig.inputSchema, handler);
2618
+ }
2619
+ const promptConfig = mcpPrompts?.get(name);
2620
+ if (promptConfig) {
2621
+ const handler = originalHandler.bind(instance);
2622
+ router.prompt(promptConfig.name || name, promptConfig.arguments, handler);
2623
+ }
2624
+ const resourceConfig = mcpResources?.get(name);
2625
+ if (resourceConfig) {
2626
+ const handler = originalHandler.bind(instance);
2627
+ router.resource(resourceConfig.uri, {
2628
+ name: resourceConfig.name || name,
2629
+ description: resourceConfig.description,
2630
+ mimeType: resourceConfig.mimeType
2631
+ }, handler);
2632
+ }
1987
2633
  }
1988
2634
  if (routesAttached === 0) {
1989
2635
  console.warn(`No routes attached to controller ${instance.constructor.name}`);
@@ -2014,71 +2660,177 @@ function getErrorStatus(err) {
2014
2660
  }
2015
2661
  return 500;
2016
2662
  }
2663
+ class NotFoundError extends HttpError {
2664
+ constructor(message = "Not Found") {
2665
+ super(message, 404);
2666
+ this.name = "NotFoundError";
2667
+ }
2668
+ }
2017
2669
  class EventError extends HttpError {
2018
2670
  constructor(message = "Event Error") {
2019
2671
  super(message, 500);
2020
2672
  this.name = "EventError";
2021
2673
  }
2022
2674
  }
2023
- class MiddlewareTracker {
2024
- static wrap(handler, context2) {
2025
- const { file, line, name, isBuiltin, pluginName } = context2;
2026
- const handlerName = name || handler.name || "anonymous";
2027
- const trackedHandler = async (ctx, next) => {
2028
- if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2029
- return handler(ctx, next);
2030
- }
2031
- const startTime = performance.now();
2032
- let error = void 0;
2033
- try {
2034
- ctx.handlerStack.push({
2035
- name: handlerName,
2036
- file,
2037
- line,
2038
- isBuiltin,
2039
- startTime,
2040
- duration: -1
2041
- });
2042
- return await handler(ctx, next);
2043
- } catch (e) {
2044
- error = e;
2045
- throw e;
2046
- } finally {
2047
- const duration = performance.now() - startTime;
2048
- const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
2049
- if (stackItem && stackItem.name === handlerName) {
2050
- stackItem.duration = duration;
2051
- }
2052
- Promise.resolve().then(async () => {
2675
+ class McpProtocol {
2676
+ tools = /* @__PURE__ */ new Map();
2677
+ prompts = /* @__PURE__ */ new Map();
2678
+ resources = /* @__PURE__ */ new Map();
2679
+ constructor(tools = [], prompts = [], resources = []) {
2680
+ tools.forEach((t) => this.tools.set(t.name, t));
2681
+ prompts.forEach((p) => this.prompts.set(p.name, p));
2682
+ resources.forEach((r) => this.resources.set(r.uri, r));
2683
+ }
2684
+ addTool(tool) {
2685
+ this.tools.set(tool.name, tool);
2686
+ }
2687
+ addPrompt(prompt) {
2688
+ this.prompts.set(prompt.name, prompt);
2689
+ }
2690
+ addResource(resource) {
2691
+ this.resources.set(resource.uri, resource);
2692
+ }
2693
+ merge(other) {
2694
+ other.tools.forEach((t) => this.tools.set(t.name, t));
2695
+ other.prompts.forEach((p) => this.prompts.set(p.name, p));
2696
+ other.resources.forEach((r) => this.resources.set(r.uri, r));
2697
+ }
2698
+ async handleMessage(message) {
2699
+ if (message.jsonrpc !== "2.0") {
2700
+ return this.error(message.id, -32600, "Invalid Request");
2701
+ }
2702
+ try {
2703
+ switch (message.method) {
2704
+ case "initialize":
2705
+ return this.success(message.id, {
2706
+ protocolVersion: "2024-11-05",
2707
+ serverInfo: {
2708
+ name: "Shokupan MCP",
2709
+ version: "1.0.0"
2710
+ },
2711
+ capabilities: {
2712
+ tools: this.tools.size > 0 ? {} : void 0,
2713
+ prompts: this.prompts.size > 0 ? {} : void 0,
2714
+ resources: this.resources.size > 0 ? {} : void 0
2715
+ }
2716
+ });
2717
+ case "ping":
2718
+ return this.success(message.id, {});
2719
+ case "tools/list":
2720
+ if (this.tools.size === 0) return this.success(message.id, { tools: [] });
2721
+ return this.success(message.id, {
2722
+ tools: Array.from(this.tools.values()).map((t) => ({
2723
+ name: t.name,
2724
+ description: t.description,
2725
+ inputSchema: t.inputSchema || { type: "object", properties: {} }
2726
+ }))
2727
+ });
2728
+ case "tools/call": {
2729
+ if (!message.params || !message.params.name) {
2730
+ return this.error(message.id, -32602, "Invalid params: name required");
2731
+ }
2732
+ const tool = this.tools.get(message.params.name);
2733
+ if (!tool) {
2734
+ return this.error(message.id, -32601, `Tool not found: ${message.params.name}`);
2735
+ }
2053
2736
  try {
2054
- const db = ctx.app?.db;
2055
- if (!db) return;
2056
- const timestamp = Date.now();
2057
- await db.upsert(new RecordId("middleware_tracking", {
2058
- timestamp,
2059
- name: handlerName
2060
- }), {
2061
- name: handlerName,
2062
- path: ctx.path,
2063
- timestamp,
2064
- duration,
2065
- file,
2066
- line,
2067
- error: error ? String(error) : void 0,
2068
- metadata: {
2069
- isBuiltin,
2070
- pluginName
2737
+ const result = await tool.handler(message.params.arguments || {});
2738
+ return this.success(message.id, result);
2739
+ } catch (e) {
2740
+ return {
2741
+ jsonrpc: "2.0",
2742
+ id: message.id ?? null,
2743
+ result: {
2744
+ isError: true,
2745
+ content: [{ type: "text", text: e.message || String(e) }]
2071
2746
  }
2072
- });
2073
- } catch (err) {
2747
+ };
2074
2748
  }
2075
- });
2749
+ }
2750
+ case "prompts/list":
2751
+ if (this.prompts.size === 0) return this.success(message.id, { prompts: [] });
2752
+ return this.success(message.id, {
2753
+ prompts: Array.from(this.prompts.values()).map((p) => ({
2754
+ name: p.name,
2755
+ description: p.description,
2756
+ arguments: p.arguments
2757
+ }))
2758
+ });
2759
+ case "prompts/get": {
2760
+ if (!message.params || !message.params.name) {
2761
+ return this.error(message.id, -32602, "Invalid params: name required");
2762
+ }
2763
+ const prompt = this.prompts.get(message.params.name);
2764
+ if (!prompt) {
2765
+ return this.error(message.id, -32601, `Prompt not found: ${message.params.name}`);
2766
+ }
2767
+ const result = await prompt.handler(message.params.arguments || {});
2768
+ return this.success(message.id, result);
2769
+ }
2770
+ case "resources/list":
2771
+ if (this.resources.size === 0) return this.success(message.id, { resources: [] });
2772
+ return this.success(message.id, {
2773
+ resources: Array.from(this.resources.values()).map((r) => ({
2774
+ uri: r.uri,
2775
+ name: r.name,
2776
+ description: r.description,
2777
+ mimeType: r.mimeType
2778
+ }))
2779
+ });
2780
+ case "resources/read": {
2781
+ if (!message.params || !message.params.uri) {
2782
+ return this.error(message.id, -32602, "Invalid params: uri required");
2783
+ }
2784
+ let resource = this.resources.get(message.params.uri);
2785
+ if (!resource) {
2786
+ return this.error(message.id, -32601, `Resource not found: ${message.params.uri}`);
2787
+ }
2788
+ const result = await resource.handler(message.params.uri);
2789
+ return this.success(message.id, result);
2790
+ }
2791
+ default:
2792
+ if (message.id === void 0) return null;
2793
+ return this.error(message.id, -32601, "Method not found");
2076
2794
  }
2795
+ } catch (err) {
2796
+ return this.error(message.id, -32603, "Internal Error", err.message);
2797
+ }
2798
+ }
2799
+ success(id, result) {
2800
+ return {
2801
+ jsonrpc: "2.0",
2802
+ id: id ?? null,
2803
+ result
2804
+ };
2805
+ }
2806
+ error(id, code, message, data) {
2807
+ return {
2808
+ jsonrpc: "2.0",
2809
+ id: id ?? null,
2810
+ error: { code, message, data }
2077
2811
  };
2078
- trackedHandler.metadata = handler.metadata || context2;
2079
- Object.defineProperty(trackedHandler, "name", { value: handlerName });
2080
- trackedHandler.originalHandler = handler.originalHandler || handler;
2081
- return trackedHandler;
2812
+ }
2813
+ }
2814
+ class MiddlewareTracker {
2815
+ static wrap(handler, context) {
2816
+ const { file: file2, line, name, isBuiltin, pluginName } = context;
2817
+ const handlerName = name || handler.name || "anonymous";
2818
+ try {
2819
+ handler.metadata = context;
2820
+ if (!handler.name || handler.name === "anonymous") {
2821
+ try {
2822
+ Object.defineProperty(handler, "name", { value: handlerName, configurable: true });
2823
+ } catch (e) {
2824
+ }
2825
+ }
2826
+ } catch (e) {
2827
+ const wrapped = handler.bind(null);
2828
+ wrapped.metadata = context;
2829
+ Object.defineProperty(wrapped, "name", { value: handlerName });
2830
+ wrapped.originalHandler = handler.originalHandler || handler;
2831
+ return wrapped;
2832
+ }
2833
+ return handler;
2082
2834
  }
2083
2835
  }
2084
2836
  class ShokupanRequestBase {
@@ -2104,6 +2856,15 @@ class ShokupanRequestBase {
2104
2856
  this.headers = new Headers(this.headers);
2105
2857
  }
2106
2858
  }
2859
+ clone() {
2860
+ return new ShokupanRequest({
2861
+ method: this.method,
2862
+ url: this.url,
2863
+ headers: new Headers(this.headers),
2864
+ body: this.body
2865
+ // Shallow copy of body, might need deep copy if object
2866
+ });
2867
+ }
2107
2868
  }
2108
2869
  const ShokupanRequest = ShokupanRequestBase;
2109
2870
  class RouterTrie {
@@ -2269,9 +3030,6 @@ class ShokupanRouter {
2269
3030
  return this._hasAfterValidateHook;
2270
3031
  }
2271
3032
  requestTimeout;
2272
- get db() {
2273
- return this.root?.db;
2274
- }
2275
3033
  hookCache = /* @__PURE__ */ new Map();
2276
3034
  hooksInitialized = false;
2277
3035
  middleware = [];
@@ -2286,6 +3044,7 @@ class ShokupanRouter {
2286
3044
  trie = new RouterTrie();
2287
3045
  metadata;
2288
3046
  // Metadata for the router itself
3047
+ mcpProtocol = new McpProtocol();
2289
3048
  currentGuards = [];
2290
3049
  eventHandlers = /* @__PURE__ */ new Map();
2291
3050
  /**
@@ -2297,7 +3056,7 @@ class ShokupanRouter {
2297
3056
  return this;
2298
3057
  }
2299
3058
  // Registry Accessor
2300
- getComponentRegistry() {
3059
+ get registry() {
2301
3060
  const controllerRoutesMap = /* @__PURE__ */ new Map();
2302
3061
  const localRoutes = [];
2303
3062
  for (let i = 0; i < this[$routes].length; i++) {
@@ -2333,7 +3092,7 @@ class ShokupanRouter {
2333
3092
  type: "router",
2334
3093
  path: r[$mountPath].startsWith("/") ? r[$mountPath] : "/" + r[$mountPath],
2335
3094
  metadata: r.metadata,
2336
- children: r.getComponentRegistry()
3095
+ children: r.registry
2337
3096
  }));
2338
3097
  const controllers = this[$childControllers].map((c) => {
2339
3098
  const routes = controllerRoutesMap.get(c) || [];
@@ -2411,6 +3170,39 @@ class ShokupanRouter {
2411
3170
  handlers.push(handler);
2412
3171
  return this;
2413
3172
  }
3173
+ /**
3174
+ * Registers an MCP Tool.
3175
+ */
3176
+ tool(name, schema, handler) {
3177
+ this.mcpProtocol.addTool({
3178
+ name,
3179
+ inputSchema: schema,
3180
+ handler
3181
+ });
3182
+ return this;
3183
+ }
3184
+ /**
3185
+ * Registers an MCP Prompt.
3186
+ */
3187
+ prompt(name, args, handler) {
3188
+ this.mcpProtocol.addPrompt({
3189
+ name,
3190
+ arguments: args,
3191
+ handler
3192
+ });
3193
+ return this;
3194
+ }
3195
+ /**
3196
+ * Registers an MCP Resource.
3197
+ */
3198
+ resource(uri, options, handler) {
3199
+ this.mcpProtocol.addResource({
3200
+ uri,
3201
+ handler,
3202
+ ...options
3203
+ });
3204
+ return this;
3205
+ }
2414
3206
  /**
2415
3207
  * Finds an event handler(s) by name.
2416
3208
  */
@@ -2428,7 +3220,7 @@ class ShokupanRouter {
2428
3220
  /**
2429
3221
  * Registers a controller instance to the router.
2430
3222
  */
2431
- registerControllerInstance(controller) {
3223
+ bindController(controller) {
2432
3224
  this[$childControllers].push(controller);
2433
3225
  }
2434
3226
  /**
@@ -2709,63 +3501,52 @@ class ShokupanRouter {
2709
3501
  }
2710
3502
  }
2711
3503
  }
2712
- let wrappedHandler = async (ctx) => {
2713
- return handler(ctx);
2714
- };
2715
- wrappedHandler.originalHandler = handler.originalHandler || handler;
2716
- const routeGuards = [...this.currentGuards];
2717
3504
  const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
2718
- if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
2719
- const originalHandler = wrappedHandler;
3505
+ const effectiveRenderer = renderer ?? this.config?.renderer ?? this.rootConfig?.renderer;
3506
+ const routeGuards = [...this.currentGuards];
3507
+ let wrappedHandler = handler;
3508
+ if (effectiveTimeout && effectiveTimeout > 0 || effectiveRenderer || routeGuards.length > 0) {
3509
+ const originalHandler = handler;
2720
3510
  wrappedHandler = async (ctx) => {
2721
- if (ctx.server) {
3511
+ if (effectiveTimeout && effectiveTimeout > 0 && ctx.server) {
2722
3512
  ctx.server.timeout(ctx.req, effectiveTimeout / 1e3);
2723
3513
  }
2724
- return originalHandler(ctx);
2725
- };
2726
- wrappedHandler.originalHandler = originalHandler.originalHandler || originalHandler;
2727
- }
2728
- if (routeGuards.length > 0) {
2729
- const innerHandler = wrappedHandler;
2730
- wrappedHandler = async (ctx) => {
2731
- for (let i = 0; i < routeGuards.length; i++) {
2732
- const guard = routeGuards[i];
2733
- let guardPassed = false;
2734
- let nextCalled = false;
2735
- const next = () => {
2736
- nextCalled = true;
2737
- return Promise.resolve();
2738
- };
2739
- try {
2740
- const result = await guard.handler(ctx, next);
2741
- if (result === true || nextCalled) {
2742
- guardPassed = true;
2743
- } else if (result !== void 0 && result !== null && result !== false) {
2744
- return result;
2745
- } else {
3514
+ if (effectiveRenderer) {
3515
+ ctx.setRenderer(effectiveRenderer);
3516
+ }
3517
+ if (routeGuards.length > 0) {
3518
+ for (let i = 0; i < routeGuards.length; i++) {
3519
+ const guard = routeGuards[i];
3520
+ let guardPassed = false;
3521
+ let nextCalled = false;
3522
+ const next = () => {
3523
+ nextCalled = true;
3524
+ return Promise.resolve();
3525
+ };
3526
+ try {
3527
+ const result = await guard.handler(ctx, next);
3528
+ if (result === true || nextCalled) {
3529
+ guardPassed = true;
3530
+ } else if (result !== void 0 && result !== null && result !== false) {
3531
+ return result;
3532
+ } else {
3533
+ return ctx.json({ error: "Forbidden" }, 403);
3534
+ }
3535
+ } catch (error) {
3536
+ throw error;
3537
+ }
3538
+ if (!guardPassed) {
2746
3539
  return ctx.json({ error: "Forbidden" }, 403);
2747
3540
  }
2748
- } catch (error) {
2749
- throw error;
2750
- }
2751
- if (!guardPassed) {
2752
- return ctx.json({ error: "Forbidden" }, 403);
2753
3541
  }
2754
3542
  }
2755
- return innerHandler(ctx);
2756
- };
2757
- }
2758
- const effectiveRenderer = renderer ?? this.config?.renderer ?? this.rootConfig?.renderer;
2759
- if (effectiveRenderer) {
2760
- const innerHandler = wrappedHandler;
2761
- wrappedHandler = async (ctx) => {
2762
- ctx.setRenderer(effectiveRenderer);
2763
- return innerHandler(ctx);
3543
+ return originalHandler(ctx);
2764
3544
  };
3545
+ wrappedHandler.originalHandler = handler.originalHandler || handler;
2765
3546
  }
2766
- const { file, line } = metadata || getCallerInfo();
3547
+ const { file: file2, line } = metadata || getCallerInfo();
2767
3548
  wrappedHandler = MiddlewareTracker.wrap(wrappedHandler, {
2768
- file,
3549
+ file: file2,
2769
3550
  line,
2770
3551
  name: handler.name || "anonymous",
2771
3552
  isBuiltin: handler.isBuiltin,
@@ -2788,7 +3569,7 @@ class ShokupanRouter {
2788
3569
  requestTimeout,
2789
3570
  renderer,
2790
3571
  metadata: {
2791
- file,
3572
+ file: file2,
2792
3573
  line
2793
3574
  },
2794
3575
  controller,
@@ -2828,7 +3609,7 @@ class ShokupanRouter {
2828
3609
  guard(specOrHandler, handler) {
2829
3610
  const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
2830
3611
  const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
2831
- let file = "unknown";
3612
+ let file2 = "unknown";
2832
3613
  let line = 0;
2833
3614
  try {
2834
3615
  const err = new Error();
@@ -2839,14 +3620,14 @@ class ShokupanRouter {
2839
3620
  if (callerLine) {
2840
3621
  const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
2841
3622
  if (match) {
2842
- file = match[1];
3623
+ file2 = match[1];
2843
3624
  line = parseInt(match[2], 10);
2844
3625
  }
2845
3626
  }
2846
3627
  } catch (e) {
2847
3628
  }
2848
3629
  const trackedGuard = MiddlewareTracker.wrap(guardHandler, {
2849
- file,
3630
+ file: file2,
2850
3631
  line,
2851
3632
  name: guardHandler.name || "guard"
2852
3633
  });
@@ -3005,68 +3786,6 @@ class ShokupanRouter {
3005
3786
  }
3006
3787
  }
3007
3788
  }
3008
- function createHttpServer() {
3009
- return async (options) => {
3010
- const server = http$1.createServer(async (req, res) => {
3011
- const url = new URL(req.url, `http://${req.headers.host}`);
3012
- const request = new Request(url.toString(), {
3013
- method: req.method,
3014
- headers: req.headers,
3015
- body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
3016
- start(controller) {
3017
- req.on("data", (chunk) => controller.enqueue(chunk));
3018
- req.on("end", () => controller.close());
3019
- req.on("error", (err) => controller.error(err));
3020
- }
3021
- }),
3022
- // Required for Node.js undici when sending a body
3023
- duplex: "half"
3024
- });
3025
- const response = await options.fetch(request, fauxServer);
3026
- res.statusCode = response.status;
3027
- response.headers.forEach((v, k) => res.setHeader(k, v));
3028
- if (response.body) {
3029
- const buffer = await response.arrayBuffer();
3030
- res.end(Buffer.from(buffer));
3031
- } else {
3032
- res.end();
3033
- }
3034
- });
3035
- const fauxServer = {
3036
- stop: () => {
3037
- server.close();
3038
- return Promise.resolve();
3039
- },
3040
- upgrade(req, options2) {
3041
- return false;
3042
- },
3043
- reload(options2) {
3044
- return fauxServer;
3045
- },
3046
- get port() {
3047
- const addr = server.address();
3048
- if (typeof addr === "object" && addr !== null) {
3049
- return addr.port;
3050
- }
3051
- return options.port;
3052
- },
3053
- hostname: options.hostname,
3054
- development: options.development,
3055
- pendingRequests: 0,
3056
- requestIP: (req) => null,
3057
- publish: () => 0,
3058
- subscriberCount: () => 0,
3059
- url: new URL(`http://${options.hostname}:${options.port}`),
3060
- // Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)
3061
- nodeServer: server
3062
- };
3063
- return new Promise((resolve2) => {
3064
- server.listen(options.port, options.hostname, () => {
3065
- resolve2(fauxServer);
3066
- });
3067
- });
3068
- };
3069
- }
3070
3789
  class BunAdapter {
3071
3790
  server;
3072
3791
  async listen(port, app) {
@@ -3203,43 +3922,161 @@ class BunAdapter {
3203
3922
  class NodeAdapter {
3204
3923
  server;
3205
3924
  async listen(port, app) {
3206
- let factory = app.applicationConfig.serverFactory;
3207
- if (!factory) {
3208
- factory = createHttpServer();
3209
- }
3210
- const serveOptions = {
3211
- port,
3212
- hostname: app.applicationConfig.hostname,
3213
- development: app.applicationConfig.development,
3214
- fetch: app.fetch.bind(app),
3215
- reusePort: app.applicationConfig.reusePort
3216
- // Node adapter might not support all options exactly the same
3217
- };
3218
- this.server = await factory(serveOptions);
3219
- return this.server;
3220
- }
3221
- async stop() {
3222
- if (this.server?.stop) {
3223
- await this.server.stop();
3224
- }
3225
- }
3226
- }
3227
- let fs;
3228
- class DefaultFileSystemAdapter {
3229
- async readFile(path) {
3230
- if (typeof Bun !== "undefined") {
3231
- return Bun.file(path);
3232
- } else {
3233
- fs ??= await import("node:fs/promises");
3234
- return fs.readFile(path);
3925
+ const factory = app.applicationConfig.serverFactory;
3926
+ let nodeServer;
3927
+ if (factory) {
3928
+ const serveOptions = {
3929
+ port,
3930
+ hostname: app.applicationConfig.hostname,
3931
+ development: app.applicationConfig.development,
3932
+ fetch: app.fetch.bind(app),
3933
+ reusePort: app.applicationConfig.reusePort
3934
+ };
3935
+ this.server = await factory(serveOptions);
3936
+ return this.server;
3235
3937
  }
3236
- }
3938
+ nodeServer = http$1.createServer(async (req, res) => {
3939
+ const url = new URL(req.url, `http://${req.headers.host}`);
3940
+ const request = new Request(url.toString(), {
3941
+ method: req.method,
3942
+ headers: req.headers,
3943
+ body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
3944
+ start(controller) {
3945
+ req.on("data", (chunk) => controller.enqueue(chunk));
3946
+ req.on("end", () => controller.close());
3947
+ req.on("error", (err) => controller.error(err));
3948
+ }
3949
+ }),
3950
+ // Required for Node.js undici when sending a body
3951
+ // @ts-ignore
3952
+ duplex: "half"
3953
+ });
3954
+ const response = await app.fetch(request, fauxServer);
3955
+ res.statusCode = response.status;
3956
+ response.headers.forEach((v, k) => res.setHeader(k, v));
3957
+ if (response.body) {
3958
+ const buffer = await response.arrayBuffer();
3959
+ res.end(Buffer.from(buffer));
3960
+ } else {
3961
+ res.end();
3962
+ }
3963
+ });
3964
+ this.server = nodeServer;
3965
+ const fauxServer = {
3966
+ stop: () => {
3967
+ nodeServer.close();
3968
+ return Promise.resolve();
3969
+ },
3970
+ upgrade(req, options) {
3971
+ return false;
3972
+ },
3973
+ reload(options) {
3974
+ return fauxServer;
3975
+ },
3976
+ get port() {
3977
+ const addr = nodeServer.address();
3978
+ if (typeof addr === "object" && addr !== null) {
3979
+ return addr.port;
3980
+ }
3981
+ return port;
3982
+ },
3983
+ hostname: app.applicationConfig.hostname || "localhost",
3984
+ development: app.applicationConfig.development || false,
3985
+ pendingRequests: 0,
3986
+ requestIP: (req) => null,
3987
+ publish: () => 0,
3988
+ subscriberCount: () => 0,
3989
+ url: new URL(`http://${app.applicationConfig.hostname || "localhost"}:${port}`),
3990
+ // Expose the raw Node.js server
3991
+ // @ts-ignore
3992
+ nodeServer
3993
+ };
3994
+ return new Promise((resolve2) => {
3995
+ nodeServer.listen(port, app.applicationConfig.hostname, () => {
3996
+ resolve2(fauxServer);
3997
+ });
3998
+ });
3999
+ }
4000
+ async stop() {
4001
+ if (this.server?.stop) {
4002
+ await this.server.stop();
4003
+ } else if (this.server?.close) {
4004
+ this.server.close();
4005
+ }
4006
+ }
4007
+ }
4008
+ class ShokupanServer {
4009
+ constructor(app) {
4010
+ this.app = app;
4011
+ }
4012
+ server;
4013
+ adapter;
4014
+ /**
4015
+ * Starts the application server.
4016
+ * @param port The port to listen on.
4017
+ */
4018
+ async listen(port) {
4019
+ const config = this.app.applicationConfig;
4020
+ const finalPort = port ?? config.port ?? 3e3;
4021
+ if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
4022
+ throw new Error("Invalid port number");
4023
+ }
4024
+ await this.app.start();
4025
+ let adapterName = config.adapter;
4026
+ let adapter;
4027
+ if (!adapterName) {
4028
+ if (typeof Bun !== "undefined") {
4029
+ config.adapter = "bun";
4030
+ adapter = new BunAdapter();
4031
+ } else {
4032
+ config.adapter = "node";
4033
+ adapter = new NodeAdapter();
4034
+ }
4035
+ } else if (adapterName === "bun") {
4036
+ adapter = new BunAdapter();
4037
+ } else if (adapterName === "node") {
4038
+ adapter = new NodeAdapter();
4039
+ } else if (adapterName === "wintercg") {
4040
+ throw new Error("WinterCG adapter does not support listen(). Use fetch directly.");
4041
+ } else {
4042
+ adapter = new NodeAdapter();
4043
+ }
4044
+ this.adapter = adapter;
4045
+ this.app.compile();
4046
+ this.server = await adapter.listen(finalPort, this.app);
4047
+ if (finalPort === 0 && this.server?.port) {
4048
+ config.port = this.server.port;
4049
+ }
4050
+ return this.server;
4051
+ }
4052
+ /**
4053
+ * Stops the server.
4054
+ */
4055
+ async stop(closeActiveConnections) {
4056
+ if (this.adapter?.stop) {
4057
+ await this.adapter.stop();
4058
+ } else if (this.server?.stop) {
4059
+ await this.server.stop(closeActiveConnections);
4060
+ }
4061
+ this.server = void 0;
4062
+ }
4063
+ }
4064
+ let fs;
4065
+ class DefaultFileSystemAdapter {
4066
+ async readFile(path) {
4067
+ if (typeof Bun !== "undefined") {
4068
+ return Bun.file(path);
4069
+ } else {
4070
+ fs ??= await import("node:fs/promises");
4071
+ return fs.readFile(path);
4072
+ }
4073
+ }
3237
4074
  async stat(path) {
3238
4075
  if (typeof Bun !== "undefined") {
3239
- const file = Bun.file(path);
4076
+ const file2 = Bun.file(path);
3240
4077
  return {
3241
- size: file.size,
3242
- mtime: new Date(file.lastModified)
4078
+ size: file2.size,
4079
+ mtime: new Date(file2.lastModified)
3243
4080
  };
3244
4081
  } else {
3245
4082
  fs ??= await import("node:fs/promises");
@@ -3406,12 +4243,39 @@ class SurrealDatastore {
3406
4243
  return this.db.close();
3407
4244
  }
3408
4245
  }
4246
+ const kContext = /* @__PURE__ */ Symbol("kContext");
4247
+ let patched = false;
4248
+ function enablePromisePatch() {
4249
+ if (patched) return;
4250
+ patched = true;
4251
+ const OriginalPromise = global.Promise;
4252
+ global.Promise = class PatchedPromise extends OriginalPromise {
4253
+ [kContext];
4254
+ constructor(executor) {
4255
+ const store = asyncContext.getStore();
4256
+ const stack = new Error().stack || "No parent stack";
4257
+ super(executor);
4258
+ this[kContext] = {
4259
+ store,
4260
+ stack
4261
+ };
4262
+ }
4263
+ };
4264
+ for (const prop of Object.getOwnPropertyNames(OriginalPromise)) {
4265
+ if (prop !== "prototype" && prop !== "length" && prop !== "name") {
4266
+ if (typeof OriginalPromise[prop] === "function") {
4267
+ global.Promise[prop] = OriginalPromise[prop];
4268
+ }
4269
+ }
4270
+ }
4271
+ }
3409
4272
  const defaults = {
3410
4273
  port: 3e3,
3411
4274
  hostname: "localhost",
3412
4275
  development: process.env.NODE_ENV !== "production",
3413
4276
  enableAsyncLocalStorage: false,
3414
4277
  enableHttpBridge: false,
4278
+ enableOpenApiGen: true,
3415
4279
  reusePort: false
3416
4280
  };
3417
4281
  class Shokupan extends ShokupanRouter {
@@ -3423,8 +4287,11 @@ class Shokupan extends ShokupanRouter {
3423
4287
  composedMiddleware;
3424
4288
  cpuMonitor;
3425
4289
  server;
4290
+ httpServer;
3426
4291
  datastore;
3427
4292
  dbPromise;
4293
+ // Performance: Flattened Router Trie
4294
+ rootTrie;
3428
4295
  get db() {
3429
4296
  return this.datastore;
3430
4297
  }
@@ -3439,17 +4306,38 @@ class Shokupan extends ShokupanRouter {
3439
4306
  this[$isApplication] = true;
3440
4307
  this[$appRoot] = this;
3441
4308
  this.applicationConfig = config;
3442
- const { file, line } = getCallerInfo();
4309
+ const { file: file2, line } = getCallerInfo();
3443
4310
  this.metadata = {
3444
- file,
4311
+ file: file2,
3445
4312
  line,
3446
4313
  name: "ShokupanApplication"
3447
4314
  };
4315
+ if (this.applicationConfig.defaultSecurityHeaders) {
4316
+ const { SecurityHeaders: SecurityHeaders2 } = require("./plugins/middleware/security-headers");
4317
+ this.use(SecurityHeaders2(this.applicationConfig.defaultSecurityHeaders === true ? {} : this.applicationConfig.defaultSecurityHeaders));
4318
+ }
3448
4319
  if (this.applicationConfig.adapter !== "wintercg") {
3449
4320
  this.dbPromise = this.initDatastore().catch((err) => {
3450
4321
  this.logger?.debug("Failed to initialize default datastore", { error: err });
3451
4322
  });
3452
4323
  }
4324
+ if (this.applicationConfig.enablePromiseMonkeypatch) {
4325
+ enablePromisePatch();
4326
+ const processRef = typeof process !== "undefined" ? process : void 0;
4327
+ if (processRef && processRef.on) {
4328
+ processRef.on("unhandledRejection", (reason, promise) => {
4329
+ const ctx = promise?.[kContext];
4330
+ if (ctx && ctx.store && ctx.store.app === this) {
4331
+ const { requestId } = ctx.store;
4332
+ this.logger.error("Unhandled Rejection in Shokupan Request", {
4333
+ error: reason,
4334
+ requestId,
4335
+ creationStack: ctx.stack
4336
+ });
4337
+ }
4338
+ });
4339
+ }
4340
+ }
3453
4341
  }
3454
4342
  async initDatastore() {
3455
4343
  let engines = this.applicationConfig.surreal?.engines;
@@ -3479,9 +4367,9 @@ class Shokupan extends ShokupanRouter {
3479
4367
  * Adds middleware to the application.
3480
4368
  */
3481
4369
  use(middleware) {
3482
- const { file, line } = getCallerInfo();
4370
+ const { file: file2, line } = getCallerInfo();
3483
4371
  const wrapped = MiddlewareTracker.wrap(middleware, {
3484
- file,
4372
+ file: file2,
3485
4373
  line,
3486
4374
  name: middleware.name || "middleware",
3487
4375
  isBuiltin: middleware.isBuiltin,
@@ -3525,16 +4413,17 @@ class Shokupan extends ShokupanRouter {
3525
4413
  * @param port - The port to listen on. If not specified, the port from the configuration is used. If that is not specified, port 3000 is used.
3526
4414
  * @returns The server instance.
3527
4415
  */
3528
- async listen(port) {
3529
- const finalPort = port ?? this.applicationConfig.port ?? 3e3;
3530
- if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
3531
- throw new Error("Invalid port number");
3532
- }
4416
+ /**
4417
+ * Prepare the application for listening.
4418
+ * Use this if you want to initialize the app without starting the server immediately.
4419
+ */
4420
+ async start() {
3533
4421
  await Promise.all(this.startupHooks.map((hook) => hook()));
3534
4422
  if (this.applicationConfig.enableOpenApiGen) {
3535
4423
  this.get("/.well-known/openapi.yaml", async (ctx) => {
3536
4424
  try {
3537
4425
  await this.openApiSpecPromise;
4426
+ const { dump } = await import("js-yaml");
3538
4427
  const yaml = dump(this.openApiSpec);
3539
4428
  return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
3540
4429
  } catch (e) {
@@ -3560,13 +4449,13 @@ class Shokupan extends ShokupanRouter {
3560
4449
  auth: config.auth || { type: "none" },
3561
4450
  api: config.api || {
3562
4451
  type: "openapi",
3563
- url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`,
4452
+ url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/.well-known/openapi.yaml`,
3564
4453
  is_user_authenticated: false
3565
4454
  },
3566
- logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/logo.png`,
4455
+ logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/logo.png`,
3567
4456
  // Placeholder default
3568
4457
  contact_email: config.contact_email || pkg.author?.email || "support@example.com",
3569
- legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/legal`
4458
+ legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/legal`
3570
4459
  };
3571
4460
  return ctx.json(manifest);
3572
4461
  });
@@ -3579,8 +4468,8 @@ class Shokupan extends ShokupanRouter {
3579
4468
  versions: config.versions || [
3580
4469
  {
3581
4470
  name: this.openApiSpec.info.version || "v1",
3582
- url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/`,
3583
- spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`
4471
+ url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/`,
4472
+ spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${this.applicationConfig.port}/.well-known/openapi.yaml`
3584
4473
  }
3585
4474
  ]
3586
4475
  };
@@ -3616,28 +4505,20 @@ class Shokupan extends ShokupanRouter {
3616
4505
  await this.asyncApiSpecPromise;
3617
4506
  }
3618
4507
  }
3619
- if (port === 0 && process.platform === "linux") ;
3620
4508
  if (this.applicationConfig.autoBackpressureFeedback === true) {
3621
4509
  this.cpuMonitor = new SystemCpuMonitor();
3622
4510
  this.cpuMonitor.start();
3623
4511
  }
3624
- let adapter = this.applicationConfig.adapter;
3625
- if (!adapter) {
3626
- if (typeof Bun !== "undefined") {
3627
- this.applicationConfig.adapter = "bun";
3628
- adapter = new BunAdapter();
3629
- } else {
3630
- this.applicationConfig.adapter = "node";
3631
- adapter = new NodeAdapter();
3632
- }
3633
- } else if (adapter === "bun") {
3634
- adapter = new BunAdapter();
3635
- } else if (adapter === "node") {
3636
- adapter = new NodeAdapter();
3637
- } else if (adapter === "wintercg") {
3638
- throw new Error("WinterCG adapter does not support listen(). Use fetch directly.");
3639
- }
3640
- this.server = await adapter.listen(finalPort, this);
4512
+ }
4513
+ /**
4514
+ * Starts the application server.
4515
+ *
4516
+ * @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.
4517
+ * @returns The server instance.
4518
+ */
4519
+ async listen(port) {
4520
+ this.httpServer = new ShokupanServer(this);
4521
+ this.server = await this.httpServer.listen(port);
3641
4522
  return this.server;
3642
4523
  }
3643
4524
  /**
@@ -3663,10 +4544,14 @@ class Shokupan extends ShokupanRouter {
3663
4544
  this.cpuMonitor.stop();
3664
4545
  this.cpuMonitor = void 0;
3665
4546
  }
3666
- if (this.server) {
4547
+ if (this.httpServer !== void 0) {
4548
+ await this.httpServer.stop(closeActiveConnections);
4549
+ } else if (this.server?.stop) {
3667
4550
  await this.server.stop(closeActiveConnections);
3668
- this.server = void 0;
3669
4551
  }
4552
+ this.server = void 0;
4553
+ const { Container: Container2 } = await Promise.resolve().then(() => di);
4554
+ await Container2.teardown();
3670
4555
  }
3671
4556
  [$dispatch](req) {
3672
4557
  return this.fetch(req);
@@ -3675,6 +4560,9 @@ class Shokupan extends ShokupanRouter {
3675
4560
  * Processes a request by wrapping the standard fetch method.
3676
4561
  */
3677
4562
  async testRequest(options) {
4563
+ if (!this.rootTrie) {
4564
+ this.compile();
4565
+ }
3678
4566
  let url = options.url || options.path || "/";
3679
4567
  if (!url.startsWith("http")) {
3680
4568
  const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
@@ -3727,7 +4615,8 @@ class Shokupan extends ShokupanRouter {
3727
4615
  */
3728
4616
  async fetch(req, server) {
3729
4617
  if (this.applicationConfig.enableTracing) {
3730
- const tracer2 = trace.getTracer("shokupan.application");
4618
+ const { trace, context } = await import("@opentelemetry/api");
4619
+ const tracer = trace.getTracer("shokupan.application");
3731
4620
  const store = asyncContext.getStore();
3732
4621
  const attrs = {
3733
4622
  attributes: {
@@ -3737,7 +4626,7 @@ class Shokupan extends ShokupanRouter {
3737
4626
  };
3738
4627
  const parent = store?.span;
3739
4628
  const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
3740
- return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
4629
+ return tracer.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
3741
4630
  const ctxStore = new RequestContextStore();
3742
4631
  ctxStore.span = span;
3743
4632
  ctxStore.request = req;
@@ -3745,16 +4634,19 @@ class Shokupan extends ShokupanRouter {
3745
4634
  });
3746
4635
  }
3747
4636
  if (this.applicationConfig.enableAsyncLocalStorage) {
4637
+ const requestId = this.applicationConfig.idGenerator?.() ?? nanoid();
3748
4638
  const ctxStore = new RequestContextStore();
3749
4639
  ctxStore.request = req;
3750
- return asyncContext.run(ctxStore, () => this.handleRequest(req, server));
4640
+ ctxStore["requestId"] = requestId;
4641
+ ctxStore["app"] = this;
4642
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server, requestId));
3751
4643
  }
3752
4644
  return this.handleRequest(req, server);
3753
4645
  }
3754
- async handleRequest(req, server) {
4646
+ async handleRequest(req, server, requestId) {
3755
4647
  const request = req;
3756
4648
  const controller = new AbortController();
3757
- const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
4649
+ const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking, requestId);
3758
4650
  const handle = async () => {
3759
4651
  if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
3760
4652
  const msg = "Too Many Requests (CPU Backpressure)";
@@ -3775,9 +4667,91 @@ class Shokupan extends ShokupanRouter {
3775
4667
  ctx[$routeMatched] = true;
3776
4668
  ctx.params = match.params;
3777
4669
  if (bodyParsing) await bodyParsing;
4670
+ if (this.applicationConfig.enableMiddlewareTracking) {
4671
+ const handler = match.handler;
4672
+ const meta = handler.metadata;
4673
+ if (meta) {
4674
+ const trackingStartTime = performance.now();
4675
+ const handlerName = meta.name || handler.name || "anonymous";
4676
+ ctx.handlerStack.push({
4677
+ name: handlerName,
4678
+ file: meta.file,
4679
+ line: meta.line,
4680
+ isBuiltin: meta.isBuiltin,
4681
+ startTime: trackingStartTime,
4682
+ duration: -1
4683
+ });
4684
+ try {
4685
+ const res = await handler(ctx);
4686
+ const duration = performance.now() - trackingStartTime;
4687
+ const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
4688
+ if (stackItem) stackItem.duration = duration;
4689
+ Promise.resolve().then(async () => {
4690
+ try {
4691
+ const db = this.db;
4692
+ if (!db) return;
4693
+ const timestamp = Date.now();
4694
+ await db.upsert(new RecordId("middleware_tracking", {
4695
+ timestamp,
4696
+ name: handlerName
4697
+ }), {
4698
+ name: handlerName,
4699
+ path: ctx.path,
4700
+ timestamp,
4701
+ duration,
4702
+ file: meta.file,
4703
+ line: meta.line,
4704
+ error: void 0,
4705
+ metadata: {
4706
+ isBuiltin: meta.isBuiltin,
4707
+ pluginName: meta.pluginName
4708
+ }
4709
+ });
4710
+ } catch (e) {
4711
+ }
4712
+ });
4713
+ return res;
4714
+ } catch (err) {
4715
+ const duration = performance.now() - trackingStartTime;
4716
+ const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
4717
+ if (stackItem) stackItem.duration = duration;
4718
+ Promise.resolve().then(async () => {
4719
+ try {
4720
+ const db = this.db;
4721
+ if (!db) return;
4722
+ const timestamp = Date.now();
4723
+ await db.upsert(new RecordId("middleware_tracking", {
4724
+ timestamp,
4725
+ name: handlerName
4726
+ }), {
4727
+ name: handlerName,
4728
+ path: ctx.path,
4729
+ timestamp,
4730
+ duration,
4731
+ file: meta.file,
4732
+ line: meta.line,
4733
+ error: String(err),
4734
+ metadata: {
4735
+ isBuiltin: meta.isBuiltin,
4736
+ pluginName: meta.pluginName
4737
+ }
4738
+ });
4739
+ } catch (e) {
4740
+ }
4741
+ });
4742
+ throw err;
4743
+ }
4744
+ }
4745
+ }
3778
4746
  return match.handler(ctx);
3779
4747
  }
3780
- return null;
4748
+ if (ctx.upgrade()) {
4749
+ return void 0;
4750
+ }
4751
+ if (ctx.response.status !== HTTP_STATUS.OK) {
4752
+ return ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
4753
+ }
4754
+ throw new NotFoundError();
3781
4755
  });
3782
4756
  let response;
3783
4757
  if (result instanceof Response) {
@@ -3790,16 +4764,13 @@ class Shokupan extends ShokupanRouter {
3790
4764
  } else if (ctx.isUpgraded) {
3791
4765
  return void 0;
3792
4766
  } else if (ctx[$routeMatched]) {
3793
- response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3794
- } else {
3795
- if (ctx.upgrade()) {
3796
- return void 0;
3797
- }
3798
- if (ctx.response.status !== HTTP_STATUS.OK) {
3799
- response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3800
- } else {
3801
- response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
4767
+ let status = ctx.response.status;
4768
+ if (status === HTTP_STATUS.OK) {
4769
+ status = HTTP_STATUS.NO_CONTENT;
3802
4770
  }
4771
+ response = ctx.send(null, { status, headers: ctx.response.headers });
4772
+ } else {
4773
+ throw new NotFoundError();
3803
4774
  }
3804
4775
  } else if (typeof result === "object") {
3805
4776
  response = ctx.json(result);
@@ -3819,8 +4790,11 @@ class Shokupan extends ShokupanRouter {
3819
4790
  if (err instanceof SyntaxError && err.message.includes("JSON")) {
3820
4791
  status = 400;
3821
4792
  }
3822
- const body = { error: err.message || "Internal Server Error" };
3823
- if (err.errors) body.errors = err.errors;
4793
+ const isDev = this.applicationConfig.development !== false;
4794
+ const message = isDev ? err.message || "Internal Server Error" : "Internal Server Error";
4795
+ const body = { error: message };
4796
+ if (isDev && err.errors) body.errors = err.errors;
4797
+ if (isDev && err.stack) body.stack = err.stack;
3824
4798
  if (this.hasOnErrorHook) await this.runHooks("onError", ctx, err);
3825
4799
  return ctx.json(body, status);
3826
4800
  }
@@ -3849,6 +4823,72 @@ class Shokupan extends ShokupanRouter {
3849
4823
  return res;
3850
4824
  });
3851
4825
  }
4826
+ /**
4827
+ * Compiles all routes into a master Trie for O(1) router lookup.
4828
+ * Use this if adding routes dynamically after start (not recommended but possible).
4829
+ */
4830
+ compile() {
4831
+ this.rootTrie = new RouterTrie();
4832
+ this.flattenRoutes(this.rootTrie, this, "", []);
4833
+ }
4834
+ flattenRoutes(trie, router, prefix, middlewareStack) {
4835
+ let effectiveStack = middlewareStack;
4836
+ if (router !== this) {
4837
+ effectiveStack = [...middlewareStack, ...router.middleware];
4838
+ }
4839
+ const joinPath = (base, segment) => {
4840
+ let b = base;
4841
+ if (b !== "/" && b.endsWith("/")) {
4842
+ b = b.slice(0, -1);
4843
+ }
4844
+ let s = segment;
4845
+ if (s === "/") {
4846
+ return b;
4847
+ }
4848
+ if (s === "") {
4849
+ return b;
4850
+ }
4851
+ if (!s.startsWith("/")) {
4852
+ s = "/" + s;
4853
+ }
4854
+ if (b === "/") {
4855
+ return s;
4856
+ }
4857
+ return b + s;
4858
+ };
4859
+ for (const route of router[$routes]) {
4860
+ const fullPath = joinPath(prefix, route.path);
4861
+ let handler = route.bakedHandler || route.handler;
4862
+ if (effectiveStack.length > 0) {
4863
+ const fn = compose(effectiveStack);
4864
+ const originalHandler = handler;
4865
+ handler = async (ctx) => {
4866
+ return fn(ctx, () => originalHandler(ctx));
4867
+ };
4868
+ handler.originalHandler = originalHandler.originalHandler || originalHandler;
4869
+ }
4870
+ trie.insert(route.method, fullPath, handler);
4871
+ if ((route.path === "/" || route.path === "") && fullPath !== "/") {
4872
+ trie.insert(route.method, fullPath + "/", handler);
4873
+ }
4874
+ }
4875
+ for (const child of router[$childRouters]) {
4876
+ const mountPath = child[$mountPath];
4877
+ const childPrefix = joinPath(prefix, mountPath);
4878
+ this.flattenRoutes(trie, child, childPrefix, effectiveStack);
4879
+ }
4880
+ }
4881
+ find(method, path) {
4882
+ if (this.rootTrie) {
4883
+ const result = this.rootTrie.search(method, path);
4884
+ if (result) return result;
4885
+ if (method === "HEAD") {
4886
+ return this.rootTrie.search("GET", path);
4887
+ }
4888
+ return null;
4889
+ }
4890
+ return super.find(method, path);
4891
+ }
3852
4892
  }
3853
4893
  function RateLimitMiddleware(options = {}) {
3854
4894
  const windowMs = options.windowMs || 60 * 1e3;
@@ -3858,6 +4898,7 @@ function RateLimitMiddleware(options = {}) {
3858
4898
  const headers = options.headers !== false;
3859
4899
  const mode = options.mode || "user";
3860
4900
  const trustedProxies = options.trustedProxies || [];
4901
+ const cleanupInterval = options.cleanupInterval || windowMs;
3861
4902
  const keyGenerator = options.keyGenerator || ((ctx) => {
3862
4903
  if (mode === "absolute") {
3863
4904
  return "global";
@@ -3887,7 +4928,7 @@ function RateLimitMiddleware(options = {}) {
3887
4928
  hits.delete(key);
3888
4929
  }
3889
4930
  }
3890
- }, windowMs);
4931
+ }, cleanupInterval);
3891
4932
  if (interval.unref) interval.unref();
3892
4933
  const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
3893
4934
  if (skip(ctx)) return next();
@@ -3944,8 +4985,79 @@ function Controller(path = "/") {
3944
4985
  target[$controllerPath] = path;
3945
4986
  };
3946
4987
  }
3947
- function Use(...middleware) {
3948
- return (target, propertyKey, descriptor) => {
4988
+ function Injectable(scope = "singleton") {
4989
+ return (target) => {
4990
+ Reflect.defineMetadata("di:scope", scope, target);
4991
+ };
4992
+ }
4993
+ function Inject(token) {
4994
+ return (target, propertyKey, indexOrDescriptor) => {
4995
+ if (typeof indexOrDescriptor === "undefined" || typeof indexOrDescriptor === "object" && indexOrDescriptor !== null) {
4996
+ const key = String(propertyKey);
4997
+ Object.defineProperty(target, key, {
4998
+ get: () => Container.resolve(token),
4999
+ enumerable: true,
5000
+ configurable: true
5001
+ });
5002
+ return;
5003
+ }
5004
+ if (typeof indexOrDescriptor === "number") {
5005
+ const index = indexOrDescriptor;
5006
+ const existing = Reflect.getMetadata("di:constructor:params", target) || [];
5007
+ existing.push({ index, token });
5008
+ Reflect.defineMetadata("di:constructor:params", existing, target);
5009
+ }
5010
+ };
5011
+ }
5012
+ function Use(tokenOrMiddleware, ...moreMiddleware) {
5013
+ return (target, propertyKey, indexOrDescriptor) => {
5014
+ if (typeof indexOrDescriptor === "number") {
5015
+ const index = indexOrDescriptor;
5016
+ if (!propertyKey) {
5017
+ let token2 = tokenOrMiddleware;
5018
+ if (!token2) {
5019
+ const paramTypes = Reflect.getMetadata("design:paramtypes", target);
5020
+ if (paramTypes && paramTypes[index]) {
5021
+ token2 = paramTypes[index];
5022
+ }
5023
+ }
5024
+ const existing = Reflect.getMetadata("di:constructor:params", target) || [];
5025
+ existing.push({ index, token: token2 });
5026
+ Reflect.defineMetadata("di:constructor:params", existing, target);
5027
+ return;
5028
+ }
5029
+ if (!target[$routeArgs]) target[$routeArgs] = /* @__PURE__ */ new Map();
5030
+ if (!target[$routeArgs].has(propertyKey)) target[$routeArgs].set(propertyKey, []);
5031
+ let token = tokenOrMiddleware;
5032
+ if (!token) {
5033
+ const paramTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey);
5034
+ if (paramTypes && paramTypes[index]) {
5035
+ token = paramTypes[index];
5036
+ }
5037
+ }
5038
+ target[$routeArgs].get(propertyKey).push({
5039
+ index,
5040
+ type: RouteParamType.SERVICE,
5041
+ token
5042
+ });
5043
+ return;
5044
+ }
5045
+ if (typeof propertyKey === "string" && indexOrDescriptor === void 0) {
5046
+ let token = tokenOrMiddleware;
5047
+ if (!token) {
5048
+ token = Reflect.getMetadata("design:type", target, propertyKey);
5049
+ }
5050
+ Object.defineProperty(target, propertyKey, {
5051
+ get: () => {
5052
+ if (!token) throw new Error(`Cannot resolve dependency for ${target.constructor.name}.${propertyKey} - no token provided and types unavailable.`);
5053
+ return Container.resolve(token);
5054
+ },
5055
+ enumerable: true,
5056
+ configurable: true
5057
+ });
5058
+ return;
5059
+ }
5060
+ const middleware = [tokenOrMiddleware, ...moreMiddleware];
3949
5061
  if (!propertyKey) {
3950
5062
  const existing = target[$middleware] || [];
3951
5063
  target[$middleware] = [...existing, ...middleware];
@@ -4025,7 +5137,38 @@ function Event(eventName) {
4025
5137
  function RateLimit(options) {
4026
5138
  return Use(RateLimitMiddleware(options));
4027
5139
  }
4028
- function ApiExplorerApp({ spec, asyncSpec, config }) {
5140
+ function Tool(options) {
5141
+ return (target, propertyKey, descriptor) => {
5142
+ target[$mcpTools] ??= /* @__PURE__ */ new Map();
5143
+ target[$mcpTools].set(propertyKey, {
5144
+ name: options?.name,
5145
+ description: options?.description,
5146
+ inputSchema: options?.inputSchema
5147
+ });
5148
+ };
5149
+ }
5150
+ function Prompt(options) {
5151
+ return (target, propertyKey, descriptor) => {
5152
+ target[$mcpPrompts] ??= /* @__PURE__ */ new Map();
5153
+ target[$mcpPrompts].set(propertyKey, {
5154
+ name: options?.name,
5155
+ description: options?.description,
5156
+ arguments: options?.arguments
5157
+ });
5158
+ };
5159
+ }
5160
+ function Resource(uri, options) {
5161
+ return (target, propertyKey, descriptor) => {
5162
+ target[$mcpResources] ??= /* @__PURE__ */ new Map();
5163
+ target[$mcpResources].set(propertyKey, {
5164
+ uri,
5165
+ name: options?.name,
5166
+ description: options?.description,
5167
+ mimeType: options?.mimeType
5168
+ });
5169
+ };
5170
+ }
5171
+ function ApiExplorerApp({ spec, base, asyncSpec, config }) {
4029
5172
  const hierarchy = /* @__PURE__ */ new Map();
4030
5173
  const addRoute = (groupKey, route) => {
4031
5174
  if (!hierarchy.has(groupKey)) {
@@ -4211,8 +5354,8 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
4211
5354
  /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
4212
5355
  /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
4213
5356
  /* @__PURE__ */ jsx("link", { href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Vend+Sans:ital,wght@0,300..700;1,300..700&display=swap", rel: "stylesheet" }),
4214
- /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "style.css" }),
4215
- /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "theme.css" }),
5357
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
5358
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
4216
5359
  /* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
4217
5360
  /* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
4218
5361
  /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
@@ -4232,7 +5375,7 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
4232
5375
  /* @__PURE__ */ jsx(Sidebar$1, { spec, hierarchicalGroups }),
4233
5376
  /* @__PURE__ */ jsx(MainContent$1, { allRoutes, config, spec })
4234
5377
  ] }),
4235
- /* @__PURE__ */ jsx("script", { src: "explorer-client.mjs", type: "module" })
5378
+ /* @__PURE__ */ jsx("script", { src: `${base}/explorer-client.mjs`, type: "module" })
4236
5379
  ] })
4237
5380
  ] });
4238
5381
  }
@@ -4392,8 +5535,8 @@ class ApiExplorerPlugin extends ShokupanRouter {
4392
5535
  return dir;
4393
5536
  }
4394
5537
  init() {
4395
- const serveFile = async (ctx, file, type) => {
4396
- const content = await readFile$1(join$1(ApiExplorerPlugin.getBasePath(), "static", file), "utf-8");
5538
+ const serveFile = async (ctx, file2, type) => {
5539
+ const content = await readFile$1(join$1(ApiExplorerPlugin.getBasePath(), "static", file2), "utf-8");
4397
5540
  ctx.set("Content-Type", type);
4398
5541
  return ctx.send(content);
4399
5542
  };
@@ -4416,10 +5559,16 @@ class ApiExplorerPlugin extends ShokupanRouter {
4416
5559
  this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
4417
5560
  this.get("/explorer-client.mjs", (ctx) => serveFile(ctx, "explorer-client.mjs", "application/javascript"));
4418
5561
  this.get("/_source", async (ctx) => {
4419
- const file = ctx.query["file"];
4420
- if (!file) return ctx.text("Missing file parameter", 400);
5562
+ const file2 = ctx.query["file"];
5563
+ if (!file2) return ctx.text("Missing file parameter", 400);
5564
+ const { resolve: resolve2, normalize, isAbsolute } = await import("node:path");
5565
+ const cwd = process.cwd();
5566
+ const resolvedPath = resolve2(cwd, file2);
5567
+ if (!resolvedPath.startsWith(cwd)) {
5568
+ return ctx.text("Forbidden: File must be within project root", 403);
5569
+ }
4421
5570
  try {
4422
- const content = await readFile$1(file, "utf-8");
5571
+ const content = await readFile$1(resolvedPath, "utf-8");
4423
5572
  return ctx.text(content);
4424
5573
  } catch (err) {
4425
5574
  return ctx.text("File not found", 404);
@@ -4432,7 +5581,8 @@ class ApiExplorerPlugin extends ShokupanRouter {
4432
5581
  this.get("/", async (ctx) => {
4433
5582
  const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
4434
5583
  const asyncSpec = ctx.app.asyncApiSpec;
4435
- const element = ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec });
5584
+ const base = this.pluginOptions.path;
5585
+ const element = ApiExplorerApp({ spec: stripSourceCode(spec), base, asyncSpec });
4436
5586
  const html = renderToString(element);
4437
5587
  if (html.length === 0) throw new Error("ApiExplorerPlugin: rendered page is blank.");
4438
5588
  return ctx.html(html);
@@ -4474,12 +5624,12 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
4474
5624
  ] });
4475
5625
  }
4476
5626
  function Sidebar({ navTree, disableSourceView }) {
4477
- return /* @__PURE__ */ jsxs("div", { class: "sidebar scroller", id: "sidebar", children: [
5627
+ return /* @__PURE__ */ jsxs("div", { class: "sidebar", id: "sidebar", children: [
4478
5628
  /* @__PURE__ */ jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
4479
5629
  /* @__PURE__ */ jsx("h2", { children: "AsyncAPI" }),
4480
5630
  /* @__PURE__ */ jsx("button", { id: "btn-collapse-nav", class: "btn-icon", title: "Collapse Sidebar", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) }) })
4481
5631
  ] }),
4482
- /* @__PURE__ */ jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
5632
+ /* @__PURE__ */ jsx("div", { class: "nav-list scroller", id: "nav-list", children: /* @__PURE__ */ jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
4483
5633
  ] });
4484
5634
  }
4485
5635
  function NavNode({ node, level, disableSourceView }) {
@@ -4649,7 +5799,7 @@ async function generateAsyncApi(rootRouter, options = {}) {
4649
5799
  let astMiddlewareRegistry = {};
4650
5800
  let applications = [];
4651
5801
  try {
4652
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-CnKnQ5KV.js");
5802
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-B0fMzeIo.js");
4653
5803
  const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
4654
5804
  const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
4655
5805
  const analysisResult = await analyzer.analyze();
@@ -5022,8 +6172,8 @@ class AsyncApiPlugin extends ShokupanRouter {
5022
6172
  }
5023
6173
  }
5024
6174
  init() {
5025
- const serveFile = async (ctx, file, type) => {
5026
- const content = await readFile(join$1(AsyncApiPlugin.getBasePath(), "static", file), "utf-8");
6175
+ const serveFile = async (ctx, file2, type) => {
6176
+ const content = await readFile(join$1(AsyncApiPlugin.getBasePath(), "static", file2), "utf-8");
5027
6177
  ctx.set("Content-Type", type);
5028
6178
  return ctx.send(content);
5029
6179
  };
@@ -5055,12 +6205,18 @@ class AsyncApiPlugin extends ShokupanRouter {
5055
6205
  return ctx.json(spec);
5056
6206
  });
5057
6207
  this.get("/_code", async (ctx) => {
5058
- const file = ctx.query["file"];
5059
- if (!file || typeof file !== "string") {
6208
+ const file2 = ctx.query["file"];
6209
+ if (!file2 || typeof file2 !== "string") {
5060
6210
  return ctx.text("Missing file parameter", 400);
5061
6211
  }
6212
+ const { resolve: resolve2 } = await import("node:path");
6213
+ const cwd = process.cwd();
6214
+ const resolvedPath = resolve2(cwd, file2);
6215
+ if (!resolvedPath.startsWith(cwd)) {
6216
+ return ctx.text("Forbidden: File must be within project root", 403);
6217
+ }
5062
6218
  try {
5063
- const content = await readFile(file, "utf8");
6219
+ const content = await readFile(resolvedPath, "utf8");
5064
6220
  return ctx.text(content);
5065
6221
  } catch (e) {
5066
6222
  return ctx.text("File not found: " + e.message, 404);
@@ -5115,7 +6271,7 @@ class AuthPlugin extends ShokupanRouter {
5115
6271
  }
5116
6272
  }
5117
6273
  async createSession(user, ctx) {
5118
- const alg = "HS256";
6274
+ const alg = this.authConfig.jwtAlgorithm || "HS256";
5119
6275
  const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
5120
6276
  const opts = this.authConfig.cookieOptions || {};
5121
6277
  let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
@@ -5417,7 +6573,7 @@ class ClusterPlugin {
5417
6573
  }
5418
6574
  }
5419
6575
  }
5420
- function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern }) {
6576
+ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern, ignorePaths }) {
5421
6577
  return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
5422
6578
  /* @__PURE__ */ jsxs("head", { children: [
5423
6579
  /* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
@@ -5527,22 +6683,22 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
5527
6683
  /* @__PURE__ */ jsx("option", { value: "ws", children: "WS" }),
5528
6684
  /* @__PURE__ */ jsx("option", { value: "other", children: "Other" })
5529
6685
  ] }),
6686
+ /* @__PURE__ */ jsxs("div", { style: "display: flex; align-items: center; gap: 4px; background: var(--bg-primary); padding: 0 8px; border: 1px solid var(--card-border); border-radius: 4px; color: var(--text-primary);", children: [
6687
+ /* @__PURE__ */ jsx("input", { type: "checkbox", id: "network-filter-ignore", checked: true }),
6688
+ /* @__PURE__ */ jsx("label", { for: "network-filter-ignore", style: "cursor: pointer; font-size: 0.9em; user-select: none;", children: "Excl. Ignored" })
6689
+ ] }),
5530
6690
  /* @__PURE__ */ jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Refresh" }),
5531
6691
  /* @__PURE__ */ jsx("button", { onclick: "purgeRequests()", style: "background: var(--bg-primary); color: var(--color-error, #ef4444); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Purge" })
5532
6692
  ] }) }),
5533
- /* @__PURE__ */ jsx("div", { id: "network-view", class: "active", style: "display: block; height: calc(100vh - 170px);", children: /* @__PURE__ */ jsxs("div", { style: "margin: 0 2rem; display: flex; gap: 1rem; height: 100%;", children: [
6693
+ /* @__PURE__ */ jsx("div", { id: "network-view", class: "active", style: "display: block; height: 100%; margin-bottom: 2rem; overflow: hidden;", children: /* @__PURE__ */ jsxs("div", { style: "margin: 0 2rem; display: flex; gap: 1rem; height: 100%;", children: [
5534
6694
  /* @__PURE__ */ jsx("div", { id: "requests-list-container", style: "flex: 1; height: 100%; border-radius: 6px; overflow: hidden; border: 1px solid var(--card-border);" }),
5535
- /* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none; width: 500px; height: 100%; overflow-y: auto; flex-shrink: 0; background: var(--bg-secondary); border: 1px solid var(--card-border); position: relative;", children: [
6695
+ /* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none; width: 500px; height: 100%; overflow: hidden; flex-shrink: 0; background: var(--bg-secondary); border: 1px solid var(--card-border); position: relative;", children: [
5536
6696
  /* @__PURE__ */ jsx("div", { id: "details-drag-handle", style: "position: absolute; left: 0; top: 0; bottom: 0; width: 5px; cursor: col-resize; z-index: 11; background: transparent;" }),
5537
6697
  /* @__PURE__ */ jsxs("div", { style: "display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; background: var(--bg-secondary); padding: 0.5rem 1rem; border-bottom: 1px solid var(--border-color); z-index: 10;", children: [
5538
- /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin: 0;", children: "Request Details" }),
6698
+ /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin: 0; padding: 0", children: "Request Details" }),
5539
6699
  /* @__PURE__ */ jsx("button", { onclick: "closeRequestDetails()", style: "background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.2rem;", children: "×" })
5540
6700
  ] }),
5541
- /* @__PURE__ */ jsxs("div", { style: "padding: 1rem;", children: [
5542
- /* @__PURE__ */ jsx("div", { id: "request-details-content" }),
5543
- /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
5544
- /* @__PURE__ */ jsx("div", { id: "middleware-trace-container" })
5545
- ] })
6701
+ /* @__PURE__ */ jsx("div", { style: "display: flex; flex-direction: column; overflow: hidden; height: 100%", children: /* @__PURE__ */ jsx("div", { id: "request-details-content", style: "flex: 1; display: flex; flex-direction: column; height: 100%; overflow: hidden" }) })
5546
6702
  ] })
5547
6703
  ] }) })
5548
6704
  ] }),
@@ -5557,7 +6713,8 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
5557
6713
  const getRequestHeaders = ${getRequestHeadersSource};
5558
6714
  window.SHOKUPAN_CONFIG = {
5559
6715
  rootPath: "${rootPath || ""}",
5560
- linkPattern: "${linkPattern || ""}"
6716
+ linkPattern: "${linkPattern || ""}",
6717
+ ignorePaths: ${JSON.stringify(ignorePaths || [])}
5561
6718
  };
5562
6719
  `
5563
6720
  } }),
@@ -5566,7 +6723,6 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
5566
6723
  /* @__PURE__ */ jsx("script", { src: `${base}/charts.js` }),
5567
6724
  /* @__PURE__ */ jsx("script", { src: `${base}/tables.js` }),
5568
6725
  /* @__PURE__ */ jsx("script", { src: `${base}/registry.js` }),
5569
- /* @__PURE__ */ jsx("script", { src: `${base}/failures.js` }),
5570
6726
  /* @__PURE__ */ jsx("script", { src: `${base}/requests.js` }),
5571
6727
  /* @__PURE__ */ jsx("script", { src: `${base}/tabs.js` })
5572
6728
  ] })
@@ -5635,15 +6791,51 @@ const require$1 = createRequire(import.meta.url);
5635
6791
  const http = require$1("node:http");
5636
6792
  const https = require$1("node:https");
5637
6793
  class FetchInterceptor {
6794
+ static originalFetch;
6795
+ static originalHttpRequest;
6796
+ static originalHttpsRequest;
5638
6797
  originalFetch;
5639
6798
  originalHttpRequest;
5640
6799
  originalHttpsRequest;
5641
6800
  callbacks = [];
5642
6801
  isPatched = false;
5643
6802
  constructor() {
5644
- this.originalFetch = global.fetch;
5645
- this.originalHttpRequest = http.request;
5646
- this.originalHttpsRequest = https.request;
6803
+ if (!FetchInterceptor.originalFetch) {
6804
+ if (global.fetch.__isPatched) {
6805
+ console.warn("[FetchInterceptor] Global fetch is already patched! Cannot capture original.");
6806
+ } else {
6807
+ FetchInterceptor.originalFetch = global.fetch;
6808
+ FetchInterceptor.originalHttpRequest = http.request;
6809
+ FetchInterceptor.originalHttpsRequest = https.request;
6810
+ }
6811
+ }
6812
+ this.originalFetch = FetchInterceptor.originalFetch || global.fetch;
6813
+ this.originalHttpRequest = FetchInterceptor.originalHttpRequest || http.request;
6814
+ this.originalHttpsRequest = FetchInterceptor.originalHttpsRequest || https.request;
6815
+ }
6816
+ /**
6817
+ * Statically restore the original network methods.
6818
+ * Useful for cleaning up in tests.
6819
+ */
6820
+ /**
6821
+ * Statically restore the original network methods.
6822
+ * Useful for cleaning up in tests.
6823
+ */
6824
+ static restore() {
6825
+ if (FetchInterceptor.originalFetch) {
6826
+ global.fetch = FetchInterceptor.originalFetch;
6827
+ } else if (global.fetch?.__originalFetch) {
6828
+ global.fetch = global.fetch.__originalFetch;
6829
+ } else if (typeof Bun !== "undefined" && Bun.fetch) {
6830
+ global.fetch = Bun.fetch;
6831
+ }
6832
+ if (FetchInterceptor.originalHttpRequest) {
6833
+ http.request = FetchInterceptor.originalHttpRequest;
6834
+ }
6835
+ if (FetchInterceptor.originalHttpsRequest) {
6836
+ https.request = FetchInterceptor.originalHttpsRequest;
6837
+ }
6838
+ console.log("[FetchInterceptor] Network layer restored (static).");
5647
6839
  }
5648
6840
  /**
5649
6841
  * Patches the global `fetch` function to intercept requests.
@@ -5658,37 +6850,33 @@ class FetchInterceptor {
5658
6850
  }
5659
6851
  patchGlobalFetch() {
5660
6852
  const self = this;
6853
+ if (!this.originalFetch && global.fetch.__isPatched && global.fetch.__originalFetch) {
6854
+ this.originalFetch = global.fetch.__originalFetch;
6855
+ }
5661
6856
  const newFetch = async function(input, init) {
5662
6857
  const startTime = performance.now();
5663
6858
  const timestamp = Date.now();
5664
- let method = "GET";
5665
6859
  let url = "";
6860
+ let method = "GET";
5666
6861
  let requestHeaders = {};
5667
- let requestBody = void 0;
5668
6862
  try {
5669
- if (input instanceof URL$1) {
5670
- url = input.toString();
5671
- } else if (typeof input === "string") {
6863
+ if (typeof input === "string") {
5672
6864
  url = input;
5673
- } else if (typeof input === "object" && "url" in input) {
6865
+ } else if (input instanceof URL$1) {
6866
+ url = input.toString();
6867
+ } else if (input instanceof Request) {
5674
6868
  url = input.url;
5675
6869
  method = input.method;
6870
+ input.headers.forEach((v, k) => requestHeaders[k] = v);
5676
6871
  }
5677
6872
  if (init) {
5678
- if (init.method) method = init.method;
6873
+ if (init.method) method = init.method.toUpperCase();
5679
6874
  if (init.headers) {
5680
- if (init.headers instanceof Headers) {
5681
- init.headers.forEach((v, k) => requestHeaders[k] = v);
5682
- } else if (Array.isArray(init.headers)) {
5683
- init.headers.forEach(([k, v]) => requestHeaders[k] = v);
5684
- } else {
5685
- Object.assign(requestHeaders, init.headers);
5686
- }
6875
+ const h = new Headers(init.headers);
6876
+ h.forEach((v, k) => requestHeaders[k] = v);
5687
6877
  }
5688
- if (init.body) requestBody = init.body;
5689
6878
  }
5690
6879
  } catch (e) {
5691
- console.warn("[FetchInterceptor] Failed to parse request arguments", e);
5692
6880
  }
5693
6881
  try {
5694
6882
  const response = await self.originalFetch.apply(global, [input, init]);
@@ -5698,14 +6886,11 @@ class FetchInterceptor {
5698
6886
  method,
5699
6887
  url,
5700
6888
  requestHeaders,
5701
- requestBody,
5702
- status: response.status,
5703
6889
  startTime: timestamp,
5704
6890
  duration,
5705
- ...self.extractRequestMeta(url, requestHeaders),
5706
- protocol: "1.1"
5707
- // native fetch doesn't expose this easily, assume 1.1/2
5708
- });
6891
+ status: response.status,
6892
+ ...self.extractRequestMeta(url, requestHeaders)
6893
+ }).catch((err) => console.error("[FetchInterceptor] Error processing response:", err));
5709
6894
  return response;
5710
6895
  } catch (error) {
5711
6896
  const duration = performance.now() - startTime;
@@ -5713,17 +6898,18 @@ class FetchInterceptor {
5713
6898
  method,
5714
6899
  url,
5715
6900
  requestHeaders,
5716
- requestBody,
5717
6901
  status: 0,
5718
6902
  responseHeaders: {},
5719
- responseBody: `Network Error: ${String(error)}`,
5720
6903
  startTime: timestamp,
5721
- duration
6904
+ duration,
6905
+ responseBody: `Error: ${error.message}`,
6906
+ ...self.extractRequestMeta(url, requestHeaders)
5722
6907
  });
5723
6908
  throw error;
5724
6909
  }
5725
6910
  };
5726
- Object.assign(newFetch, this.originalFetch);
6911
+ newFetch.__isPatched = true;
6912
+ newFetch.__originalFetch = this.originalFetch;
5727
6913
  global.fetch = newFetch;
5728
6914
  }
5729
6915
  patchNodeRequests() {
@@ -6068,6 +7254,9 @@ class Dashboard {
6068
7254
  this.broadcastMetricUpdate(metric);
6069
7255
  };
6070
7256
  this.metricsCollector = new MetricsCollector(this.db, onCollect);
7257
+ if (app.applicationConfig) {
7258
+ app.applicationConfig.enableMiddlewareTracking = true;
7259
+ }
6071
7260
  const fetchInterceptor = new FetchInterceptor();
6072
7261
  fetchInterceptor.patch();
6073
7262
  fetchInterceptor.on((log) => {
@@ -6101,6 +7290,10 @@ class Dashboard {
6101
7290
  responseHeaders: log.responseHeaders
6102
7291
  // No handler stack for outbound
6103
7292
  };
7293
+ const maxLogs = this.dashboardConfig.maxLogEntries ?? 1e3;
7294
+ if (this.metrics.logs.length >= maxLogs) {
7295
+ this.metrics.logs.shift();
7296
+ }
6104
7297
  this.metrics.logs.push(requestData);
6105
7298
  const recordId = new RecordId("request", nanoid());
6106
7299
  const idString = recordId.toString();
@@ -6128,17 +7321,9 @@ class Dashboard {
6128
7321
  }
6129
7322
  this.mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
6130
7323
  const hooks = this.getHooks();
6131
- if (!app.middleware) {
6132
- app.middleware = [];
7324
+ if (hooks.onRequestStart) {
7325
+ app.hook("onRequestStart", hooks.onRequestStart);
6133
7326
  }
6134
- const hooksMiddleware = async (ctx, next) => {
6135
- if (hooks.onRequestStart) {
6136
- await hooks.onRequestStart(ctx);
6137
- }
6138
- ctx._startTime = performance.now();
6139
- await next();
6140
- };
6141
- app.use(hooksMiddleware);
6142
7327
  if (hooks.onResponseEnd) {
6143
7328
  app.hook("onResponseEnd", hooks.onResponseEnd);
6144
7329
  }
@@ -6385,7 +7570,7 @@ class Dashboard {
6385
7570
  if (!this.instrumented && app) {
6386
7571
  this.instrumentApp(app);
6387
7572
  }
6388
- const registry = app?.getComponentRegistry?.();
7573
+ const registry = app?.registry;
6389
7574
  if (registry) {
6390
7575
  this.assignIdsToRegistry(registry, "root");
6391
7576
  }
@@ -6422,26 +7607,51 @@ class Dashboard {
6422
7607
  });
6423
7608
  this.router.post("/replay", async (ctx) => {
6424
7609
  const body = await ctx.body();
6425
- const app = this[$appRoot];
6426
- if (!app) return unknownError(ctx);
6427
- try {
6428
- const result = await app.processRequest({
6429
- method: body.method,
6430
- path: body.url,
6431
- // or path
6432
- headers: body.headers,
6433
- body: body.body
6434
- });
6435
- return ctx.json({
6436
- status: result.status,
6437
- headers: result.headers,
6438
- data: result.data
6439
- });
6440
- } catch (e) {
6441
- return ctx.json({ error: String(e) }, 500);
6442
- }
6443
- });
6444
- this.router.get("/**", async (ctx) => {
7610
+ const direction = body.direction || "inbound";
7611
+ if (direction === "outbound") {
7612
+ const start = performance.now();
7613
+ try {
7614
+ const res = await fetch(body.url, {
7615
+ method: body.method,
7616
+ headers: body.headers,
7617
+ body: body.body ? typeof body.body === "object" ? JSON.stringify(body.body) : body.body : void 0
7618
+ });
7619
+ const text = await res.text();
7620
+ const duration = performance.now() - start;
7621
+ const resHeaders = {};
7622
+ res.headers.forEach((v, k) => resHeaders[k] = v);
7623
+ return ctx.json({
7624
+ status: res.status,
7625
+ statusText: res.statusText,
7626
+ headers: resHeaders,
7627
+ data: text,
7628
+ duration
7629
+ });
7630
+ } catch (e) {
7631
+ return ctx.json({ error: String(e) }, 500);
7632
+ }
7633
+ } else {
7634
+ const app = this[$appRoot];
7635
+ if (!app) return unknownError(ctx);
7636
+ try {
7637
+ const result = await app.internalRequest({
7638
+ method: body.method,
7639
+ path: body.url,
7640
+ // or path
7641
+ headers: body.headers,
7642
+ body: body.body
7643
+ });
7644
+ return ctx.json({
7645
+ status: result.status,
7646
+ headers: result.headers,
7647
+ data: result.body
7648
+ });
7649
+ } catch (e) {
7650
+ return ctx.json({ error: String(e) }, 500);
7651
+ }
7652
+ }
7653
+ });
7654
+ this.router.get("/**", async (ctx) => {
6445
7655
  const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
6446
7656
  let relativePath = ctx.path;
6447
7657
  if (relativePath.startsWith(mountPath)) {
@@ -6476,6 +7686,14 @@ class Dashboard {
6476
7686
  const linkPattern = this.getLinkPattern();
6477
7687
  const integrations = this.detectIntegrations();
6478
7688
  const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
7689
+ const ignorePaths = [
7690
+ ...this.dashboardConfig.ignorePaths || [],
7691
+ // Add default ignores for integrations
7692
+ ...Object.values(integrations).filter((p) => !!p).flatMap((p) => {
7693
+ const clean = p.endsWith("/") ? p.slice(0, -1) : p;
7694
+ return [clean, `${clean}/**`];
7695
+ })
7696
+ ];
6479
7697
  const html = renderToString(DashboardApp({
6480
7698
  metrics: this.metrics,
6481
7699
  uptime,
@@ -6483,7 +7701,8 @@ class Dashboard {
6483
7701
  linkPattern,
6484
7702
  integrations,
6485
7703
  base: mountPath,
6486
- getRequestHeadersSource
7704
+ getRequestHeadersSource,
7705
+ ignorePaths
6487
7706
  }));
6488
7707
  return ctx.html(`<!DOCTYPE html>${html}`);
6489
7708
  });
@@ -6630,12 +7849,15 @@ class Dashboard {
6630
7849
  getHooks() {
6631
7850
  return {
6632
7851
  onRequestStart: (ctx) => {
7852
+ if (ctx.path.startsWith(this.mountPath)) return;
6633
7853
  const app = this[$appRoot];
6634
7854
  if (!this.instrumented && app) {
6635
7855
  this.instrumentApp(app);
6636
7856
  }
6637
7857
  this.metrics.totalRequests++;
6638
7858
  this.metrics.activeRequests++;
7859
+ ctx._startTime = performance.now();
7860
+ ctx._reqStartTime = Date.now();
6639
7861
  ctx[$debug] = new Collector(this);
6640
7862
  if (!this.broadcastTimer) {
6641
7863
  this.broadcastTimer = setTimeout(() => {
@@ -6725,7 +7947,7 @@ class Dashboard {
6725
7947
  url: ctx.url.toString(),
6726
7948
  status: response.status,
6727
7949
  duration,
6728
- timestamp: Date.now(),
7950
+ timestamp: ctx._reqStartTime || Date.now() - duration,
6729
7951
  handlerStack: this.serializeHandlerStack(ctx.handlerStack),
6730
7952
  body: this.serializeBody(ctx.responseBody),
6731
7953
  requestBody: ctx.bodyData || ctx.requestBody,
@@ -6746,17 +7968,12 @@ class Dashboard {
6746
7968
  responseHeaders: resHeaders
6747
7969
  };
6748
7970
  this.metrics.logs.push(logEntry);
6749
- try {
6750
- await this.db.query("UPSERT $id CONTENT $data", {
6751
- id: new RecordId("request", ctx.requestId),
6752
- data: {
6753
- ...logEntry,
6754
- direction: "inbound"
6755
- }
6756
- });
6757
- } catch (e) {
7971
+ this.db.create(new RecordId("request", ctx.requestId), {
7972
+ ...logEntry,
7973
+ direction: "inbound"
7974
+ }).catch((e) => {
6758
7975
  console.error("Failed to record request log", e);
6759
- }
7976
+ });
6760
7977
  const retention = this.dashboardConfig.retentionMs ?? 72e5;
6761
7978
  const cutoff = Date.now() - retention;
6762
7979
  if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
@@ -6854,6 +8071,463 @@ class Dashboard {
6854
8071
  function unknownError(ctx) {
6855
8072
  return ctx.json({ error: "Unknown Error" }, 500);
6856
8073
  }
8074
+ let isPatched = false;
8075
+ function applyMonkeyPatch() {
8076
+ if (isPatched) return;
8077
+ isPatched = true;
8078
+ Error.stackTraceLimit = 50;
8079
+ }
8080
+ async function readSourceContext(filePath, line, contextLines = 5) {
8081
+ if (!filePath || filePath.startsWith("node:") || filePath.startsWith("bun:") || filePath.includes("node_modules")) {
8082
+ return null;
8083
+ }
8084
+ const path = filePath.startsWith("file://") ? filePath.slice(7) : filePath;
8085
+ try {
8086
+ const f = file(path);
8087
+ if (!await f.exists()) return null;
8088
+ const content = await f.text();
8089
+ const allLines = content.split("\n");
8090
+ const targetIndex = line - 1;
8091
+ if (targetIndex < 0 || targetIndex >= allLines.length) return null;
8092
+ const start = Math.max(0, targetIndex - contextLines);
8093
+ const end = Math.min(allLines.length, targetIndex + contextLines + 1);
8094
+ const subset = allLines.slice(start, end).map((code, i) => ({
8095
+ line: start + i + 1,
8096
+ code,
8097
+ isTarget: start + i + 1 === line
8098
+ }));
8099
+ return {
8100
+ lines: subset,
8101
+ startLine: start + 1,
8102
+ file: path
8103
+ };
8104
+ } catch (e) {
8105
+ return null;
8106
+ }
8107
+ }
8108
+ async function renderErrorView(ctx, error) {
8109
+ const frames = [];
8110
+ const cwd = process.cwd();
8111
+ const errorName = error?.name || "Error";
8112
+ const errorMessage = error?.message || "Unknown error occurred";
8113
+ const errorId = error?.id || ctx.requestId || "unknown-id";
8114
+ const errorTimestamp = error?.timestamp ? new Date(error.timestamp).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
8115
+ const errorScope = error?.scope || {};
8116
+ const lines = (error?.stack || "").split("\n").slice(1);
8117
+ for (const line of lines) {
8118
+ const match = line.match(/at (?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+))\)?/);
8119
+ if (match) {
8120
+ const [_, method, file2, lineNo, colNo] = match;
8121
+ const fileName = file2 || "";
8122
+ let relativeFile = fileName;
8123
+ if (fileName.startsWith(cwd)) {
8124
+ relativeFile = fileName.slice(cwd.length + 1);
8125
+ }
8126
+ let isInternal = fileName.startsWith("node:") || fileName.startsWith("bun:") || fileName === "undefined" || fileName === "";
8127
+ if (isInternal && (method.includes("setTimeout") || method.includes("setInterval") || method.includes("setImmediate"))) {
8128
+ isInternal = false;
8129
+ }
8130
+ let isShokupan = false;
8131
+ if (fileName.includes("node_modules/@dotglitch/shokupan")) {
8132
+ isShokupan = true;
8133
+ } else if (relativeFile.startsWith("src/") || fileName.includes("/shokupan/dist/")) {
8134
+ isShokupan = true;
8135
+ }
8136
+ const isDependency = fileName.includes("node_modules") && !isShokupan;
8137
+ frames.push({
8138
+ method: method || "<anonymous>",
8139
+ file: fileName,
8140
+ line: parseInt(lineNo),
8141
+ column: parseInt(colNo),
8142
+ isNative: false,
8143
+ isInternal,
8144
+ isShokupan,
8145
+ isDependency,
8146
+ shortFile: fileName.split("/").pop() || fileName,
8147
+ relativeFile
8148
+ });
8149
+ }
8150
+ }
8151
+ let focusFrame = frames.find((f) => !f.isInternal && !f.isShokupan && !f.isDependency && !f.isNative);
8152
+ if (!focusFrame) focusFrame = frames[0];
8153
+ let sourceContext = null;
8154
+ if (focusFrame && focusFrame.file && !focusFrame.isInternal) {
8155
+ sourceContext = await readSourceContext(focusFrame.file, focusFrame.line, 8);
8156
+ }
8157
+ const renderFrames = frames.map((frame, index) => {
8158
+ const classes = [
8159
+ "stack-entry",
8160
+ frame.isInternal ? "internal" : "",
8161
+ frame.isShokupan ? "shokupan" : "",
8162
+ frame.isDependency ? "dependency" : "",
8163
+ frame === focusFrame ? "active" : ""
8164
+ ].join(" ");
8165
+ const fileLink = `vscode://file/${frame.file}:${frame.line}:${frame.column}`;
8166
+ return `
8167
+ <li class="${classes}">
8168
+ <a href="${fileLink}" style="text-decoration:none; color:inherit; display:block">
8169
+ <div class="stack-method">${frame.method === "<anonymous>" ? "Anonymous" : frame.method}</div>
8170
+ <div class="stack-file">${frame.relativeFile}:${frame.line}</div>
8171
+ </a>
8172
+ </li>
8173
+ `;
8174
+ }).join("");
8175
+ const highlightCode = (code) => {
8176
+ 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>');
8177
+ };
8178
+ if (sourceContext) {
8179
+ sourceContext.lines.map((l) => `
8180
+ <div class="code-line ${l.isTarget ? "target" : ""}">
8181
+ <div class="line-number">${l.line}</div>
8182
+ <div class="line-content">${highlightCode(l.code)}</div>
8183
+ </div>
8184
+ `).join("");
8185
+ }
8186
+ const renderKV = (data) => {
8187
+ if (!data || Object.keys(data).length === 0) return '<div style="color:var(--text-muted)">None</div>';
8188
+ return `<table class="kv-table">
8189
+ ${Object.entries(data).map(([k, v]) => {
8190
+ let displayVal = String(v);
8191
+ let valClass = "";
8192
+ if (typeof v === "number") {
8193
+ valClass = "kv-val-number";
8194
+ } else if (typeof v === "boolean") {
8195
+ valClass = "kv-val-bool";
8196
+ } else if (typeof v === "object" && v !== null) {
8197
+ try {
8198
+ displayVal = JSON.stringify(v, null, 2);
8199
+ valClass = "kv-val-json";
8200
+ } catch (e) {
8201
+ displayVal = "[Circular]";
8202
+ }
8203
+ }
8204
+ return `
8205
+ <tr>
8206
+ <td class="kv-key">${k}</td>
8207
+ <td class="kv-val ${valClass}">${displayVal}</td>
8208
+ </tr>`;
8209
+ }).join("")}
8210
+ </table>`;
8211
+ };
8212
+ 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>`;
8213
+ return `<!DOCTYPE html>
8214
+ <html lang="en">
8215
+ <head>
8216
+ <meta charset="UTF-8">
8217
+ <title>${errorName}: ${errorMessage}</title>
8218
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css" rel="stylesheet" />
8219
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-highlight/prism-line-highlight.min.css" rel="stylesheet" />
8220
+ <link href="/_shokupan/error-view/prismjs.theme.css" rel="stylesheet" />
8221
+ <link href="/_shokupan/error-view/styles.css" rel="stylesheet" />
8222
+ <link href="/_shokupan/error-view/theme.css" rel="stylesheet" />
8223
+
8224
+ </head>
8225
+ <body class="">
8226
+
8227
+ <div class="page">
8228
+ <!-- HEADER -->
8229
+ <header class="chapter-header">
8230
+ <div class="chapter-meta">
8231
+ <div class="meta-item">
8232
+ <span>${ctx.method}</span>
8233
+ </div>
8234
+ <div class="meta-item">
8235
+ <span>${ctx.url.pathname}</span>
8236
+ </div>
8237
+ <div class="meta-item">
8238
+ <span>${ctx.response.status || 500}</span>
8239
+ </div>
8240
+ <div class="meta-item" style="margin-left:auto">
8241
+ <span class="id-badge" onclick="copyText('${errorId}')" title="Copy ID">ID: ${errorId}</span>
8242
+ </div>
8243
+ </div>
8244
+
8245
+ <h1 class="error-title">${errorName}</h1>
8246
+
8247
+ <div class="error-message-container">
8248
+ <h2 class="error-message">${errorMessage}</h2>
8249
+ <button class="action-btn" onclick="copyText('${errorMessage.replace(/'/g, "\\'")}')" title="Copy Message" style="padding:4px 8px">
8250
+ ${ICON_COPY}
8251
+ </button>
8252
+ </div>
8253
+
8254
+ <div class="actions-bar">
8255
+ <button class="action-btn" onclick="copyText()">
8256
+ ${ICON_COPY} Copy Error
8257
+ </button>
8258
+ <button class="action-btn" onclick="document.getElementById('raw-modal').style.display='flex'">
8259
+ View Raw Error
8260
+ </button>
8261
+ </div>
8262
+ </header>
8263
+
8264
+ <!-- CODE FIGURE -->
8265
+ <section class="figure">
8266
+ <div class="figure-caption">
8267
+ ${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>` : ""}
8268
+ </div>
8269
+ <div class="figure-body">
8270
+ ${sourceContext ? `
8271
+ <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>
8272
+ ` : `<div style="padding: 2rem; color: var(--text-muted); text-align: center;">Source code not available.</div>`}
8273
+ </div>
8274
+ </section>
8275
+
8276
+ <!-- NARRATIVE STACK -->
8277
+ <section class="narrative">
8278
+ <div class="section-title">
8279
+ <span>Stack Trace</span>
8280
+ <div class="filter-group">
8281
+ <span class="badge" onclick="this.classList.toggle('active'); document.body.classList.toggle('show-internals')">Internals</span>
8282
+ <span class="badge" onclick="this.classList.toggle('active'); document.body.classList.toggle('show-shokupan')">Framework</span>
8283
+ <span class="badge" onclick="this.classList.toggle('active'); document.body.classList.toggle('show-dependencies')">Dependencies</span>
8284
+ </div>
8285
+ </div>
8286
+ <ul class="stack-list">
8287
+ ${renderFrames}
8288
+ </ul>
8289
+ </section>
8290
+
8291
+ <!-- APPENDICES -->
8292
+ <section class="appendix">
8293
+ <div class="section-title">Context & Environment</div>
8294
+ <div class="appendix-grid">
8295
+ <div class="data-block">
8296
+ <h3>Request</h3>
8297
+ ${renderKV({
8298
+ id: errorId,
8299
+ timestamp: errorTimestamp,
8300
+ ...errorScope || {}
8301
+ })}
8302
+ </div>
8303
+ <div class="data-block">
8304
+ <h3>Headers</h3>
8305
+ ${renderKV(Object.fromEntries(ctx.headers))}
8306
+ </div>
8307
+ <div class="data-block">
8308
+ <h3>Query & Params</h3>
8309
+ ${renderKV({ ...ctx.params, ...ctx.query })}
8310
+ </div>
8311
+ </div>
8312
+ </section>
8313
+ </div>
8314
+
8315
+ <!-- RAW ERROR MODAL -->
8316
+ <div id="raw-modal" class="modal-overlay" onclick="if(event.target === this) this.style.display='none'">
8317
+ <div class="modal-content">
8318
+ <div class="modal-header">
8319
+ <span>Raw Error Object</span>
8320
+ <button class="action-btn" onclick="document.getElementById('raw-modal').style.display='none'">Close</button>
8321
+ </div>
8322
+ <div class="modal-body" id="raw-content"></div>
8323
+ </div>
8324
+ </div>
8325
+
8326
+ <!-- PrismJS Scripts -->
8327
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"><\/script>
8328
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"><\/script>
8329
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"><\/script>
8330
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-highlight/prism-line-highlight.min.js"><\/script>
8331
+
8332
+ <script>
8333
+ // Prepare Raw Error
8334
+ // circular ref safe stringify
8335
+ const getCircularReplacer = () => {
8336
+ const seen = new WeakSet();
8337
+ return (key, value) => {
8338
+ if (typeof value === "object" && value !== null) {
8339
+ if (seen.has(value)) {
8340
+ return "[Circular]";
8341
+ }
8342
+ seen.add(value);
8343
+ }
8344
+ return value;
8345
+ };
8346
+ };
8347
+
8348
+ // Inject error data from SERVER side
8349
+ const rawError = ${(() => {
8350
+ const serializeError = (err) => {
8351
+ const obj = {
8352
+ name: err.name,
8353
+ message: err.message,
8354
+ stack: err.stack,
8355
+ ...err
8356
+ // Spread enumerable props
8357
+ };
8358
+ if (err.cause) obj.cause = err.cause;
8359
+ if (err.code) obj.code = err.code;
8360
+ if (err.status) obj.status = err.status;
8361
+ if (err.statusCode) obj.statusCode = err.statusCode;
8362
+ return JSON.stringify(obj, (key, value) => {
8363
+ if (key === "structuredStack") return void 0;
8364
+ return value;
8365
+ }, 2);
8366
+ };
8367
+ return serializeError(error);
8368
+ })()};
8369
+
8370
+ // At this point 'rawError' is an Object in Client JS (because serializeError returned a JSON string)
8371
+ const RAW_ERROR_JSON = JSON.stringify(rawError, getCircularReplacer(), 2);
8372
+ // "Normally printed" usually means standard stacktrace string which includes name/message
8373
+ const RAW_ERROR_TEXT = rawError.stack || (rawError.name + ': ' + rawError.message);
8374
+
8375
+ document.getElementById('raw-content').innerText = RAW_ERROR_JSON;
8376
+
8377
+ function copyText(text) {
8378
+ if (!text) text = RAW_ERROR_TEXT; // Default to text representation (Message + Stack)
8379
+ navigator.clipboard.writeText(text).then(() => {
8380
+ console.log('Copied');
8381
+ });
8382
+ }
8383
+ <\/script>
8384
+ </body>
8385
+ </html>`;
8386
+ }
8387
+ function renderStatusView(ctx, status, error) {
8388
+ const title = `${status} ${error.message || "Error"}`;
8389
+ const method = ctx.method;
8390
+ const path = ctx.url.pathname;
8391
+ const css = `
8392
+ body {
8393
+ background: var(--bg-primary);
8394
+ color: var(--text-primary);
8395
+ font-family: var(--shokupan-font);
8396
+ display: flex;
8397
+ align-items: center;
8398
+ justify-content: center;
8399
+ height: 100vh;
8400
+ margin: 0;
8401
+ overflow: hidden;
8402
+ }
8403
+ .container {
8404
+ text-align: center;
8405
+ animation: fadeIn 0.3s ease-out;
8406
+ background: var(--bg-card);
8407
+ padding: 3rem 4rem;
8408
+ border-radius: 16px;
8409
+ border: 1px solid var(--card-border);
8410
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
8411
+ max-width: 600px;
8412
+ }
8413
+ h1 {
8414
+ font-size: 6rem;
8415
+ margin: 0;
8416
+ color: var(--primary);
8417
+ line-height: 1;
8418
+ font-weight: 800;
8419
+ letter-spacing: -2px;
8420
+ text-shadow: 0 4px 20px rgba(255, 179, 128, 0.2);
8421
+ }
8422
+ h2 {
8423
+ font-size: 1.5rem;
8424
+ margin: 1rem 0 2rem 0;
8425
+ font-weight: 400;
8426
+ color: var(--text-secondary);
8427
+ }
8428
+ .meta {
8429
+ color: var(--text-muted);
8430
+ font-family: var(--shokupan-font-mono);
8431
+ font-size: 1rem;
8432
+ background: var(--bg-primary);
8433
+ padding: 0.75rem 1.5rem;
8434
+ border-radius: 8px;
8435
+ display: inline-block;
8436
+ border: 1px solid var(--border-color);
8437
+ }
8438
+ .method {
8439
+ font-weight: bold;
8440
+ margin-right: 0.5rem;
8441
+ padding: 0.2rem 0.5rem;
8442
+ border-radius: 4px;
8443
+ }
8444
+ .path {
8445
+ color: var(--text-primary);
8446
+ }
8447
+ @keyframes fadeIn {
8448
+ from { opacity: 0; transform: translateY(20px); }
8449
+ to { opacity: 1; transform: translateY(0); }
8450
+ }
8451
+ `;
8452
+ return `<!DOCTYPE html>
8453
+ <html lang="en">
8454
+ <head>
8455
+ <meta charset="UTF-8">
8456
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8457
+ <title>${title}</title>
8458
+ <link href="/_shokupan/error-view/theme.css" rel="stylesheet" />
8459
+ <style>${css}</style>
8460
+ </head>
8461
+ <body>
8462
+ <div class="container">
8463
+ <h1>${status}</h1>
8464
+ <h2>${error.message || "An error occurred"}</h2>
8465
+ <div class="meta">
8466
+ <span class="method badge-${method}">${method}</span>
8467
+ <span class="path">${path}</span>
8468
+ </div>
8469
+ </div>
8470
+ </body>
8471
+ </html>`;
8472
+ }
8473
+ class ErrorView {
8474
+ constructor(config = {}) {
8475
+ this.config = config;
8476
+ }
8477
+ name = "error-view";
8478
+ async onInit(app) {
8479
+ applyMonkeyPatch();
8480
+ const errorViewMiddleware = async (ctx, next) => {
8481
+ try {
8482
+ return await next();
8483
+ } catch (err) {
8484
+ const accept = ctx.get("accept") || "";
8485
+ if (!accept.includes("text/html")) {
8486
+ throw err;
8487
+ }
8488
+ if (!err.timestamp) {
8489
+ Object.defineProperty(err, "timestamp", {
8490
+ value: Date.now(),
8491
+ enumerable: false,
8492
+ writable: true,
8493
+ configurable: true
8494
+ });
8495
+ }
8496
+ if (!err.id) {
8497
+ Object.defineProperty(err, "id", {
8498
+ value: ctx.requestId,
8499
+ enumerable: false,
8500
+ writable: true,
8501
+ configurable: true
8502
+ });
8503
+ }
8504
+ if (!err.scope) {
8505
+ const store = asyncContext.getStore();
8506
+ if (store) {
8507
+ Object.defineProperty(err, "scope", {
8508
+ value: { ...store },
8509
+ enumerable: false,
8510
+ writable: true,
8511
+ configurable: true
8512
+ });
8513
+ }
8514
+ }
8515
+ const status = getErrorStatus(err);
8516
+ if (status === 404 || status === 401 || status === 403) {
8517
+ const html2 = await renderStatusView(ctx, status, err);
8518
+ return ctx.html(html2, status);
8519
+ }
8520
+ const html = await renderErrorView(ctx, err);
8521
+ return ctx.html(html, status);
8522
+ }
8523
+ };
8524
+ Object.defineProperty(errorViewMiddleware, "name", { value: "ErrorViewMiddleware" });
8525
+ const { join: join2 } = await import("path");
8526
+ const assetDir = join2(import.meta.dir, "assets");
8527
+ app.static("/_shokupan/error-view", assetDir);
8528
+ app.use(errorViewMiddleware);
8529
+ }
8530
+ }
6857
8531
  class GraphQLApolloPlugin extends ShokupanRouter {
6858
8532
  // Use generic any or verify type
6859
8533
  constructor(pluginOptions) {
@@ -6928,6 +8602,377 @@ class GraphQLApolloPlugin extends ShokupanRouter {
6928
8602
  });
6929
8603
  }
6930
8604
  }
8605
+ class GraphQLYogaPlugin extends ShokupanRouter {
8606
+ constructor(pluginOptions) {
8607
+ super();
8608
+ this.pluginOptions = pluginOptions;
8609
+ this.pluginOptions.path ??= "/graphql";
8610
+ }
8611
+ yoga;
8612
+ async onInit(app, options) {
8613
+ const { createYoga } = await import("graphql-yoga");
8614
+ const path = options?.path || this.pluginOptions.path || "/graphql";
8615
+ this.yoga = createYoga({
8616
+ ...this.pluginOptions.yogaConfig,
8617
+ graphqlEndpoint: path
8618
+ });
8619
+ app.mount(path, this);
8620
+ const handler = async (ctx) => {
8621
+ let body;
8622
+ if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") {
8623
+ body = await ctx.body();
8624
+ if (typeof body === "object" && body !== null) {
8625
+ body = JSON.stringify(body);
8626
+ }
8627
+ }
8628
+ const response = await this.yoga.fetch(
8629
+ new Request(ctx.req.url, {
8630
+ method: ctx.req.method,
8631
+ headers: ctx.req.headers,
8632
+ body
8633
+ }),
8634
+ {
8635
+ ...ctx
8636
+ }
8637
+ );
8638
+ response.headers.forEach((value, key) => {
8639
+ ctx.set(key, value);
8640
+ });
8641
+ const text = await response.text();
8642
+ return ctx.send(text, {
8643
+ status: response.status
8644
+ });
8645
+ };
8646
+ this.get("/", handler);
8647
+ this.post("/", handler);
8648
+ this.get("/*", handler);
8649
+ this.post("/*", handler);
8650
+ }
8651
+ }
8652
+ class HtmxPlugin {
8653
+ async onInit(app) {
8654
+ app.use(this.middleware());
8655
+ }
8656
+ middleware() {
8657
+ return async (ctx, next) => {
8658
+ Object.defineProperty(ctx, "isHtmx", {
8659
+ get: () => ctx.req.headers.has("hx-request")
8660
+ });
8661
+ Object.defineProperty(ctx, "isHtmxBoosted", {
8662
+ get: () => ctx.req.headers.has("hx-boosted")
8663
+ });
8664
+ ctx.trigger = (event, options) => {
8665
+ let headerName = "HX-Trigger";
8666
+ if (options?.after === "settle") headerName = "HX-Trigger-After-Settle";
8667
+ if (options?.after === "swap") headerName = "HX-Trigger-After-Swap";
8668
+ let value = JSON.stringify(event);
8669
+ if (typeof event === "string") {
8670
+ value = event;
8671
+ } else {
8672
+ value = JSON.stringify(event);
8673
+ }
8674
+ ctx.set(headerName, value);
8675
+ };
8676
+ ctx.pushUrl = (url) => {
8677
+ ctx.set("HX-Push-Url", url === false ? "false" : url);
8678
+ };
8679
+ ctx.htmxRedirect = (url) => {
8680
+ ctx.set("HX-Redirect", url);
8681
+ };
8682
+ ctx.refresh = () => {
8683
+ ctx.set("HX-Refresh", "true");
8684
+ };
8685
+ return next();
8686
+ };
8687
+ }
8688
+ }
8689
+ function Idempotency(options = {}) {
8690
+ const headerName = options.header || "Idempotency-Key";
8691
+ options.ttl || 24 * 60 * 60 * 1e3;
8692
+ let RecordIdClass;
8693
+ const idempotencyMiddleware = async function IdempotencyMiddleware(ctx, next) {
8694
+ const key = ctx.headers.get(headerName);
8695
+ if (!key) {
8696
+ return next();
8697
+ }
8698
+ try {
8699
+ if (!RecordIdClass) {
8700
+ const mod = await import("surrealdb");
8701
+ RecordIdClass = mod.RecordId;
8702
+ }
8703
+ const stored = await ctx.app.db.select(new RecordIdClass("idempotency", key));
8704
+ if (stored) {
8705
+ const responseHeaders = new Headers(stored.headers);
8706
+ responseHeaders.set("X-Idempotency-Hit", "true");
8707
+ return new Response(stored.body, {
8708
+ status: stored.status,
8709
+ headers: responseHeaders
8710
+ });
8711
+ }
8712
+ } catch (e) {
8713
+ console.error("Idempotency read error:", e);
8714
+ }
8715
+ const result = await next();
8716
+ let response;
8717
+ if (result instanceof Response) {
8718
+ response = result;
8719
+ } else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
8720
+ response = ctx[$finalResponse];
8721
+ } else if (result !== null && result !== void 0) {
8722
+ if (typeof result === "object") {
8723
+ response = await ctx.json(result);
8724
+ } else {
8725
+ response = await ctx.text(String(result));
8726
+ }
8727
+ }
8728
+ if (response instanceof Response) {
8729
+ const clone = response.clone();
8730
+ const bodyText = await clone.text();
8731
+ const headers = {};
8732
+ clone.headers.forEach((v, k) => {
8733
+ headers[k] = v;
8734
+ });
8735
+ const toStore = {
8736
+ status: clone.status,
8737
+ headers,
8738
+ body: bodyText,
8739
+ timestamp: Date.now()
8740
+ };
8741
+ try {
8742
+ await ctx.app.db.upsert(new RecordIdClass("idempotency", key), toStore);
8743
+ } catch (e) {
8744
+ console.error("Idempotency write error:", e);
8745
+ }
8746
+ return response;
8747
+ }
8748
+ return result;
8749
+ };
8750
+ idempotencyMiddleware.isBuiltin = true;
8751
+ idempotencyMiddleware.pluginName = "Idempotency";
8752
+ return idempotencyMiddleware;
8753
+ }
8754
+ class MCPServerPlugin {
8755
+ constructor(options = {}) {
8756
+ this.options = options;
8757
+ options.allowIntrospection ??= true;
8758
+ options.allowToolExecution ??= true;
8759
+ options.path ??= "/mcp";
8760
+ if (!options.path.startsWith("/")) {
8761
+ options.path = "/" + options.path;
8762
+ }
8763
+ options.rootDir ??= process.cwd();
8764
+ }
8765
+ router = new ShokupanRouter();
8766
+ analyzer;
8767
+ onInit(app) {
8768
+ this[$appRoot] = app;
8769
+ this.analyzer = new OpenAPIAnalyzer(this.options.rootDir);
8770
+ if (this.options.allowIntrospection) {
8771
+ this.registerTools();
8772
+ this.registerResources();
8773
+ this.registerPrompts();
8774
+ }
8775
+ app.onStart(async () => {
8776
+ app.mount(this.options.path, this.router);
8777
+ this.collectAppMcpItems(app);
8778
+ this.setupRoutes();
8779
+ this.router.metadata = {
8780
+ file: import.meta.file,
8781
+ line: 1,
8782
+ name: "MCPServerPlugin",
8783
+ pluginName: "MCP Server"
8784
+ };
8785
+ });
8786
+ }
8787
+ collectAppMcpItems(app) {
8788
+ const collect = (router) => {
8789
+ if (router.mcpProtocol) {
8790
+ this.router.mcpProtocol.merge(router.mcpProtocol);
8791
+ }
8792
+ router[$childRouters]?.forEach(collect);
8793
+ };
8794
+ collect(app);
8795
+ }
8796
+ setupRoutes() {
8797
+ this.router.get("", (ctx) => {
8798
+ const endpointUrl = `${ctx.protocol}://${ctx.host}${this.options.path}`;
8799
+ const enc = new TextEncoder();
8800
+ return new Response(
8801
+ new ReadableStream({
8802
+ start(controller) {
8803
+ controller.enqueue(enc.encode(`event: endpoint
8804
+ data: ${JSON.stringify(endpointUrl)}
8805
+
8806
+ `));
8807
+ },
8808
+ cancel() {
8809
+ }
8810
+ }),
8811
+ {
8812
+ headers: {
8813
+ "Content-Type": "text/event-stream",
8814
+ "Cache-Control": "no-cache",
8815
+ "Connection": "keep-alive"
8816
+ }
8817
+ }
8818
+ );
8819
+ });
8820
+ this.router.post("", async (ctx) => {
8821
+ let parsedBody;
8822
+ try {
8823
+ parsedBody = await ctx.body();
8824
+ } catch (e) {
8825
+ return ctx.json({
8826
+ jsonrpc: "2.0",
8827
+ id: null,
8828
+ error: { code: -32700, message: "Parse error" }
8829
+ }, 400);
8830
+ }
8831
+ const response = await this.router.mcpProtocol.handleMessage(parsedBody);
8832
+ if (response) {
8833
+ return ctx.json(response);
8834
+ }
8835
+ return ctx.text("", 204);
8836
+ });
8837
+ }
8838
+ registerTools() {
8839
+ const ensureExecutionAllowed = () => {
8840
+ if (!this.options.allowToolExecution) {
8841
+ throw new Error("Tool execution is disabled.");
8842
+ }
8843
+ };
8844
+ this.router.tool(
8845
+ "list_endpoints",
8846
+ {},
8847
+ async () => {
8848
+ ensureExecutionAllowed();
8849
+ const { applications } = await this.analyzer.analyze();
8850
+ const endpoints = applications.flatMap(
8851
+ (app) => app.routes.map((r) => ({
8852
+ method: r.method,
8853
+ path: r.path,
8854
+ handler: r.handlerName,
8855
+ summary: r.summary
8856
+ }))
8857
+ );
8858
+ return {
8859
+ content: [{
8860
+ type: "text",
8861
+ text: JSON.stringify(endpoints, null, 2)
8862
+ }]
8863
+ };
8864
+ }
8865
+ );
8866
+ this.router.tool(
8867
+ "get_endpoint_details",
8868
+ {
8869
+ type: "object",
8870
+ properties: {
8871
+ method: { type: "string" },
8872
+ path: { type: "string" }
8873
+ },
8874
+ required: ["method", "path"]
8875
+ },
8876
+ async ({ method, path }) => {
8877
+ ensureExecutionAllowed();
8878
+ const { applications } = await this.analyzer.analyze();
8879
+ const route = applications.flatMap((app) => app.routes).find((r) => r.method.toUpperCase() === method.toUpperCase() && r.path === path);
8880
+ if (!route) {
8881
+ return {
8882
+ content: [{ type: "text", text: `Endpoint ${method} ${path} not found.` }],
8883
+ isError: true
8884
+ };
8885
+ }
8886
+ return {
8887
+ content: [{
8888
+ type: "text",
8889
+ text: JSON.stringify(route, null, 2)
8890
+ }]
8891
+ };
8892
+ }
8893
+ );
8894
+ }
8895
+ registerResources() {
8896
+ this.router.resource(
8897
+ "mcp://api/openapi.json",
8898
+ {
8899
+ name: "openapi-spec",
8900
+ mimeType: "application/json"
8901
+ },
8902
+ async (uri) => {
8903
+ const { applications } = await this.analyzer.analyze();
8904
+ const endpoints = applications.flatMap(
8905
+ (app) => app.routes.map((r) => ({
8906
+ method: r.method,
8907
+ path: r.path,
8908
+ handler: r.handlerName,
8909
+ summary: r.summary,
8910
+ requestTypes: r.requestTypes,
8911
+ responseType: r.responseType
8912
+ }))
8913
+ );
8914
+ return {
8915
+ contents: [{
8916
+ uri,
8917
+ text: JSON.stringify(endpoints, null, 2)
8918
+ }]
8919
+ };
8920
+ }
8921
+ );
8922
+ this.router.resource(
8923
+ "mcp://api/routes/{method}/{path}/source",
8924
+ {
8925
+ name: "route-source",
8926
+ mimeType: "text/typescript"
8927
+ },
8928
+ async (uri) => {
8929
+ const parts = uri.replace("mcp://", "").split("/");
8930
+ parts[2];
8931
+ throw new Error("Dynamic resource reading not fully implemented in lightweight version yet.");
8932
+ }
8933
+ );
8934
+ }
8935
+ registerPrompts() {
8936
+ this.router.prompt(
8937
+ "generate-client",
8938
+ [
8939
+ { name: "method", required: true },
8940
+ { name: "path", required: true }
8941
+ ],
8942
+ async ({ method, path }) => {
8943
+ const { applications } = await this.analyzer.analyze();
8944
+ const route = applications.flatMap((app) => app.routes).find((r) => r.method.toUpperCase() === method.toUpperCase() && r.path === path);
8945
+ if (!route) {
8946
+ return {
8947
+ messages: [{
8948
+ role: "user",
8949
+ content: {
8950
+ type: "text",
8951
+ text: `Start a new task to create a client for ${method} ${path}. The endpoint was not found in the current analysis.`
8952
+ }
8953
+ }]
8954
+ };
8955
+ }
8956
+ return {
8957
+ messages: [{
8958
+ role: "user",
8959
+ content: {
8960
+ type: "text",
8961
+ text: `Please generate a TypeScript client function for the following endpoint:
8962
+ Method: ${route.method}
8963
+ Path: ${route.path}
8964
+ Summary: ${route.summary || "N/A"}
8965
+ Request Types: ${JSON.stringify(route.requestTypes, null, 2)}
8966
+ Response Type: ${route.responseType || "unknown"}
8967
+
8968
+ Use fetch or axios. Ensure proper typing.`
8969
+ }
8970
+ }]
8971
+ };
8972
+ }
8973
+ );
8974
+ }
8975
+ }
6931
8976
  class ScalarPlugin extends ShokupanRouter {
6932
8977
  constructor(pluginOptions = {}) {
6933
8978
  pluginOptions.config ??= {};
@@ -7103,7 +9148,7 @@ class ScalarPlugin extends ShokupanRouter {
7103
9148
  try {
7104
9149
  const entrypoint = process.argv[1];
7105
9150
  console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
7106
- const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
9151
+ const analyzer = new OpenAPIAnalyzer$1(process.cwd(), entrypoint);
7107
9152
  let staticSpec = await analyzer.analyze();
7108
9153
  if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
7109
9154
  deepMerge(this.pluginOptions.baseDocument, staticSpec);
@@ -7116,10 +9161,150 @@ class ScalarPlugin extends ShokupanRouter {
7116
9161
  }
7117
9162
  }
7118
9163
  }
9164
+ function attachSocketIOBridge(io, app) {
9165
+ io.on("connection", (socket) => {
9166
+ socket.onAny(async (event, ...args) => {
9167
+ if (event === "shokupan:request" || event === "http") {
9168
+ return;
9169
+ }
9170
+ const handler = app.findEvent(event);
9171
+ if (handler) {
9172
+ const data = args[0];
9173
+ const req = new ShokupanRequest({
9174
+ url: `socketio://${app.applicationConfig.hostname || "localhost"}/event/${event}`,
9175
+ method: "POST",
9176
+ headers: new Headers({ "content-type": "application/json" }),
9177
+ body: JSON.stringify(data)
9178
+ });
9179
+ const ctx = new ShokupanContext(req, app.server);
9180
+ ctx[$ws] = socket;
9181
+ ctx.io = io;
9182
+ try {
9183
+ for (let i = 0; i < handler.length; i++) {
9184
+ await handler[i](ctx);
9185
+ }
9186
+ } catch (e) {
9187
+ await app.runHooks("onError", ctx, e);
9188
+ if (app.applicationConfig["websocketErrorHandler"]) {
9189
+ await app.applicationConfig["websocketErrorHandler"](e, ctx);
9190
+ } else {
9191
+ console.error(`Error in event ${event}:`, e);
9192
+ }
9193
+ }
9194
+ }
9195
+ });
9196
+ if (app.applicationConfig["enableHttpBridge"]) {
9197
+ socket.on("shokupan:request", async (payload, callback) => {
9198
+ try {
9199
+ const { method, path, headers, body } = payload;
9200
+ const url = new URL(path, `http://${app.applicationConfig.hostname || "localhost"}:3000`);
9201
+ const req = new Request(url.toString(), {
9202
+ method,
9203
+ headers,
9204
+ body: typeof body === "object" ? JSON.stringify(body) : body
9205
+ });
9206
+ const res = await app.fetch(req);
9207
+ let resBody = await res.text();
9208
+ try {
9209
+ resBody = JSON.parse(resBody);
9210
+ } catch {
9211
+ }
9212
+ const resHeaders = {};
9213
+ res.headers.forEach((v, k) => resHeaders[k] = v);
9214
+ if (typeof callback === "function") {
9215
+ await callback({
9216
+ status: res.status,
9217
+ headers: resHeaders,
9218
+ body: resBody
9219
+ });
9220
+ } else {
9221
+ socket.emit("shokupan:response", {
9222
+ id: payload.id,
9223
+ status: res.status,
9224
+ headers: resHeaders,
9225
+ body: resBody
9226
+ });
9227
+ }
9228
+ } catch (e) {
9229
+ if (typeof callback === "function") {
9230
+ callback({ status: 500, body: { error: e.message } });
9231
+ }
9232
+ }
9233
+ });
9234
+ }
9235
+ });
9236
+ }
9237
+ function createLimitStream(maxSize) {
9238
+ let size = 0;
9239
+ return new TransformStream({
9240
+ transform(chunk, controller) {
9241
+ size += chunk.byteLength || chunk.length;
9242
+ if (size > maxSize) {
9243
+ controller.error(new Error(`Decompressed body size exceeded limit of ${maxSize} bytes`));
9244
+ } else {
9245
+ controller.enqueue(chunk);
9246
+ }
9247
+ }
9248
+ });
9249
+ }
7119
9250
  function Compression(options = {}) {
7120
9251
  const threshold = options.threshold ?? 512;
7121
9252
  const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
9253
+ const decompress = options.decompress ?? true;
9254
+ const maxDecompressedSize = options.maxDecompressedSize ?? 10 * 1024 * 1024;
7122
9255
  const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
9256
+ const requestEncoding = ctx.headers.get("content-encoding");
9257
+ if (decompress && requestEncoding && !ctx.headers.get("content-encoding")?.includes("identity") && ctx.req.body) {
9258
+ let stream = null;
9259
+ if (requestEncoding.includes("br")) {
9260
+ const decompressor = zlib.createBrotliDecompress();
9261
+ const nodeStream = Readable.fromWeb(ctx.req.body);
9262
+ stream = Readable.toWeb(nodeStream.pipe(decompressor));
9263
+ } else if (requestEncoding.includes("gzip")) {
9264
+ if (typeof DecompressionStream !== "undefined") {
9265
+ stream = ctx.req.body.pipeThrough(new DecompressionStream("gzip"));
9266
+ } else {
9267
+ const decompressor = zlib.createGunzip();
9268
+ const nodeStream = Readable.fromWeb(ctx.req.body);
9269
+ stream = Readable.toWeb(nodeStream.pipe(decompressor));
9270
+ }
9271
+ } else if (requestEncoding.includes("deflate")) {
9272
+ if (typeof DecompressionStream !== "undefined") {
9273
+ stream = ctx.req.body.pipeThrough(new DecompressionStream("deflate"));
9274
+ } else {
9275
+ const decompressor = zlib.createInflate();
9276
+ const nodeStream = Readable.fromWeb(ctx.req.body);
9277
+ stream = Readable.toWeb(nodeStream.pipe(decompressor));
9278
+ }
9279
+ }
9280
+ if (stream) {
9281
+ const outputStream = stream.pipeThrough(createLimitStream(maxDecompressedSize));
9282
+ const originalIp = ctx.ip;
9283
+ const originalReq = ctx.req;
9284
+ const newHeaders = new Headers(originalReq.headers);
9285
+ newHeaders.delete("content-encoding");
9286
+ newHeaders.delete("content-length");
9287
+ const newReq = new Proxy(originalReq, {
9288
+ get(target, prop, receiver) {
9289
+ if (prop === "body") return outputStream;
9290
+ if (prop === "headers") return newHeaders;
9291
+ if (prop === "json") return async () => JSON.parse(await new Response(outputStream).text());
9292
+ if (prop === "text") return async () => await new Response(outputStream).text();
9293
+ if (prop === "arrayBuffer") return async () => await new Response(outputStream).arrayBuffer();
9294
+ if (prop === "blob") return async () => await new Response(outputStream).blob();
9295
+ if (prop === "formData") return async () => await new Response(outputStream).formData();
9296
+ return Reflect.get(target, prop, target);
9297
+ }
9298
+ });
9299
+ ctx.request = newReq;
9300
+ if (originalIp) {
9301
+ Object.defineProperty(ctx, "ip", {
9302
+ configurable: true,
9303
+ get: () => originalIp
9304
+ });
9305
+ }
9306
+ }
9307
+ }
7123
9308
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
7124
9309
  let method = null;
7125
9310
  if (acceptEncoding.includes("br")) method = "br";
@@ -7571,7 +9756,7 @@ function openApiValidator() {
7571
9756
  if (validators.body) {
7572
9757
  let body;
7573
9758
  try {
7574
- body = await ctx.req.json().catch(() => ({}));
9759
+ body = await ctx.body();
7575
9760
  } catch {
7576
9761
  body = {};
7577
9762
  }
@@ -7691,10 +9876,171 @@ function enableOpenApiValidation(app) {
7691
9876
  precompileValidators(app, spec);
7692
9877
  });
7693
9878
  }
9879
+ function isPrivateIP(ip) {
9880
+ const ipv4Patterns = [
9881
+ /^10\./,
9882
+ // 10.0.0.0/8
9883
+ /^172\.(1[6-9]|2[0-9]|3[01])\./,
9884
+ // 172.16.0.0/12
9885
+ /^192\.168\./,
9886
+ // 192.168.0.0/16
9887
+ /^127\./,
9888
+ // 127.0.0.0/8 (loopback)
9889
+ /^169\.254\./,
9890
+ // 169.254.0.0/16 (link-local)
9891
+ /^0\.0\.0\.0$/
9892
+ // 0.0.0.0
9893
+ ];
9894
+ const ipv6Patterns = [
9895
+ /^::1$/,
9896
+ // loopback
9897
+ /^fe80:/,
9898
+ // link-local
9899
+ /^fc00:/,
9900
+ // unique local
9901
+ /^fd00:/
9902
+ // unique local
9903
+ ];
9904
+ for (const pattern of ipv4Patterns) {
9905
+ if (pattern.test(ip)) return true;
9906
+ }
9907
+ for (const pattern of ipv6Patterns) {
9908
+ if (pattern.test(ip.toLowerCase())) return true;
9909
+ }
9910
+ return false;
9911
+ }
9912
+ function Proxy$1(options) {
9913
+ const targetUrl = new URL(options.target);
9914
+ if (!["http:", "https:"].includes(targetUrl.protocol)) {
9915
+ throw new Error("Invalid proxy target protocol. Only http and https are allowed.");
9916
+ }
9917
+ if (options.allowedHosts && options.allowedHosts.length > 0) {
9918
+ if (!options.allowedHosts.includes(targetUrl.hostname)) {
9919
+ throw new Error(`Target hostname ${targetUrl.hostname} is not in the allowed hosts list.`);
9920
+ }
9921
+ }
9922
+ if (!options.allowPrivateIPs && isPrivateIP(targetUrl.hostname)) {
9923
+ throw new Error("Proxying to private IP addresses is not allowed.");
9924
+ }
9925
+ return async (ctx, next) => {
9926
+ const req = ctx.request;
9927
+ if (options.ws && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
9928
+ const success = ctx.upgrade({
9929
+ data: {
9930
+ handler: {
9931
+ open: (ws) => handleWSOpen(ws, ctx, options, targetUrl),
9932
+ message: (ws, message) => handleWSMessage(ws, message),
9933
+ close: (ws, code, reason) => handleWSClose(ws, code, reason),
9934
+ drain: (ws) => handleWSDrain()
9935
+ }
9936
+ }
9937
+ });
9938
+ if (success) {
9939
+ return void 0;
9940
+ }
9941
+ }
9942
+ let path = ctx.url.pathname;
9943
+ if (options.pathRewrite) {
9944
+ path = options.pathRewrite(path);
9945
+ }
9946
+ const url = new URL(path + ctx.url.search, targetUrl);
9947
+ if (!["http:", "https:"].includes(url.protocol)) {
9948
+ return ctx.text("Invalid protocol in proxied URL", 400);
9949
+ }
9950
+ const headers = new Headers(req.headers);
9951
+ if (options.changeOrigin) {
9952
+ headers.set("host", targetUrl.host);
9953
+ }
9954
+ if (options.headers) {
9955
+ Object.entries(options.headers).forEach(([key, value]) => headers.set(key, value));
9956
+ }
9957
+ headers.delete("connection");
9958
+ headers.delete("keep-alive");
9959
+ headers.delete("proxy-authenticate");
9960
+ headers.delete("proxy-authorization");
9961
+ headers.delete("te");
9962
+ headers.delete("trailer");
9963
+ headers.delete("transfer-encoding");
9964
+ headers.delete("upgrade");
9965
+ const proxyReq = new Request(url.toString(), {
9966
+ method: req.method,
9967
+ headers,
9968
+ body: req.body,
9969
+ // @ts-ignore - duplex is needed for some node/bun versions for streaming bodies
9970
+ duplex: "half"
9971
+ });
9972
+ const res = await fetch(proxyReq);
9973
+ return new Response(res.body, {
9974
+ status: res.status,
9975
+ statusText: res.statusText,
9976
+ headers: res.headers
9977
+ });
9978
+ };
9979
+ }
9980
+ const wsMap = /* @__PURE__ */ new WeakMap();
9981
+ function handleWSOpen(ws, ctx, options, targetUrl) {
9982
+ let path = ctx.url.pathname;
9983
+ if (options.pathRewrite) {
9984
+ path = options.pathRewrite(path);
9985
+ }
9986
+ const url = new URL(path + ctx.url.search, targetUrl);
9987
+ url.protocol = targetUrl.protocol.replace("http", "ws");
9988
+ const headers = {};
9989
+ if (options.changeOrigin) {
9990
+ headers["Host"] = targetUrl.host;
9991
+ }
9992
+ ctx.request.headers.forEach((v, k) => {
9993
+ if (!["upgrade", "connection", "sec-websocket-key", "sec-websocket-version", "sec-websocket-extensions"].includes(k.toLowerCase())) {
9994
+ headers[k] = v;
9995
+ }
9996
+ });
9997
+ const upstream = new WebSocket(url.toString());
9998
+ wsMap.set(ws, upstream);
9999
+ const pendingMessages = [];
10000
+ let isConnected = false;
10001
+ upstream.onopen = () => {
10002
+ isConnected = true;
10003
+ while (pendingMessages.length > 0) {
10004
+ const msg = pendingMessages.shift();
10005
+ upstream.send(msg);
10006
+ }
10007
+ };
10008
+ upstream.onmessage = (event) => {
10009
+ ws.send(event.data);
10010
+ };
10011
+ upstream.onclose = (event) => {
10012
+ ws.close(event.code, event.reason);
10013
+ };
10014
+ upstream.onerror = (err) => {
10015
+ console.error("Upstream WebSocket error:", err);
10016
+ ws.close(1011, "Internal Error");
10017
+ };
10018
+ upstream._pendingRequestMessages = pendingMessages;
10019
+ upstream._isConnected = () => isConnected;
10020
+ }
10021
+ function handleWSMessage(ws, message) {
10022
+ const upstream = wsMap.get(ws);
10023
+ if (!upstream) return;
10024
+ if (upstream._isConnected && upstream._isConnected()) {
10025
+ upstream.send(message);
10026
+ } else {
10027
+ upstream._pendingRequestMessages.push(message);
10028
+ }
10029
+ }
10030
+ function handleWSClose(ws, code, reason) {
10031
+ const upstream = wsMap.get(ws);
10032
+ if (upstream) {
10033
+ if (upstream.readyState === WebSocket.OPEN) {
10034
+ upstream.close(code, reason);
10035
+ }
10036
+ wsMap.delete(ws);
10037
+ }
10038
+ }
10039
+ function handleWSDrain(ws) {
10040
+ }
7694
10041
  function SecurityHeaders(options = {}) {
7695
10042
  const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
7696
- const headers = {};
7697
- const set = (k, v) => headers[k] = v;
10043
+ const set = (k, v) => ctx.response.set(k, v);
7698
10044
  if (options.dnsPrefetchControl !== false) {
7699
10045
  const allow = options.dnsPrefetchControl?.allow;
7700
10046
  set("X-DNS-Prefetch-Control", allow ? "on" : "off");
@@ -7740,14 +10086,6 @@ function SecurityHeaders(options = {}) {
7740
10086
  }
7741
10087
  if (options.hidePoweredBy !== false) ;
7742
10088
  const response = await next();
7743
- if (response instanceof Response) {
7744
- const headerEntries = Object.entries(headers);
7745
- for (let i = 0; i < headerEntries.length; i++) {
7746
- const [k, v] = headerEntries[i];
7747
- response.headers.set(k, v);
7748
- }
7749
- return response;
7750
- }
7751
10089
  return response;
7752
10090
  };
7753
10091
  securityHeadersMiddleware.isBuiltin = true;
@@ -7912,43 +10250,56 @@ function Session(options) {
7912
10250
  }
7913
10251
  const sessObj = existing;
7914
10252
  Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
7915
- sessObj.save = (cb) => {
7916
- store.set(sessObj.id, sessObj, cb);
10253
+ sessObj.save = () => {
10254
+ return new Promise((resolve2, reject) => {
10255
+ store.set(sessObj.id, sessObj, (err) => {
10256
+ if (err) reject(err);
10257
+ else resolve2();
10258
+ });
10259
+ });
7917
10260
  };
7918
- sessObj.destroy = (cb) => {
7919
- store.destroy(sessObj.id, (err) => {
7920
- if (cb) cb(err);
10261
+ sessObj.destroy = () => {
10262
+ return new Promise((resolve2, reject) => {
10263
+ store.destroy(sessObj.id, (err) => {
10264
+ if (err) reject(err);
10265
+ else resolve2();
10266
+ });
7921
10267
  });
7922
10268
  };
7923
- sessObj.regenerate = (cb) => {
7924
- store.destroy(sessObj.id, (err) => {
7925
- sessionID = generateId(ctx);
7926
- const keys = Object.keys(sessObj);
7927
- for (let i = 0; i < keys.length; i++) {
7928
- const key = keys[i];
7929
- if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
7930
- delete sessObj[key];
10269
+ sessObj.regenerate = () => {
10270
+ return new Promise((resolve2, reject) => {
10271
+ store.destroy(sessObj.id, (err) => {
10272
+ if (err) return reject(err);
10273
+ sessionID = generateId(ctx);
10274
+ const keys = Object.keys(sessObj);
10275
+ for (let i = 0; i < keys.length; i++) {
10276
+ const key = keys[i];
10277
+ if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
10278
+ delete sessObj[key];
10279
+ }
7931
10280
  }
7932
- }
7933
- Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
7934
- if (cb) cb(err);
10281
+ Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
10282
+ resolve2();
10283
+ });
7935
10284
  });
7936
10285
  };
7937
10286
  sessObj.undefined = () => {
7938
10287
  };
7939
- sessObj.reload = (cb) => {
7940
- store.get(sessObj.id, (err, sess2) => {
7941
- if (err) return cb(err);
7942
- if (!sess2) return cb(new Error("Session not found"));
7943
- const keys = Object.keys(sessObj);
7944
- for (let i = 0; i < keys.length; i++) {
7945
- const key = keys[i];
7946
- if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
7947
- delete sessObj[key];
10288
+ sessObj.reload = () => {
10289
+ return new Promise((resolve2, reject) => {
10290
+ store.get(sessObj.id, (err, sess2) => {
10291
+ if (err) return reject(err);
10292
+ if (!sess2) return reject(new Error("Session not found"));
10293
+ const keys = Object.keys(sessObj);
10294
+ for (let i = 0; i < keys.length; i++) {
10295
+ const key = keys[i];
10296
+ if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
10297
+ delete sessObj[key];
10298
+ }
7948
10299
  }
7949
- }
7950
- Object.assign(sessObj, sess2);
7951
- cb(null);
10300
+ Object.assign(sessObj, sess2);
10301
+ resolve2();
10302
+ });
7952
10303
  });
7953
10304
  };
7954
10305
  sessObj.touch = () => {
@@ -8024,6 +10375,7 @@ export {
8024
10375
  $bodyParsed,
8025
10376
  $bodyType,
8026
10377
  $cachedBody,
10378
+ $cachedCookies,
8027
10379
  $cachedHost,
8028
10380
  $cachedHostname,
8029
10381
  $cachedOrigin,
@@ -8040,11 +10392,15 @@ export {
8040
10392
  $isApplication,
8041
10393
  $isMounted,
8042
10394
  $isRouter,
10395
+ $mcpPrompts,
10396
+ $mcpResources,
10397
+ $mcpTools,
8043
10398
  $middleware,
8044
10399
  $mountPath,
8045
10400
  $parent,
8046
10401
  $rawBody,
8047
10402
  $requestId,
10403
+ $resilienceConfig,
8048
10404
  $routeArgs,
8049
10405
  $routeMatched,
8050
10406
  $routeMethods,
@@ -8066,24 +10422,33 @@ export {
8066
10422
  Ctx,
8067
10423
  Dashboard,
8068
10424
  Delete,
10425
+ ErrorView,
8069
10426
  Event,
8070
10427
  Get,
8071
10428
  GraphQLApolloPlugin,
10429
+ GraphQLYogaPlugin,
8072
10430
  HTTPMethods,
8073
10431
  Head,
8074
10432
  Headers$1 as Headers,
10433
+ HtmxPlugin,
10434
+ Idempotency,
8075
10435
  Inject,
8076
10436
  Injectable,
10437
+ MCPServerPlugin,
8077
10438
  MemoryStore,
10439
+ OpenTelemetryPlugin,
8078
10440
  Options,
8079
10441
  Param,
8080
10442
  Patch,
8081
10443
  Post,
10444
+ Prompt,
10445
+ Proxy$1 as Proxy,
8082
10446
  Put,
8083
10447
  Query,
8084
10448
  RateLimit,
8085
10449
  RateLimitMiddleware,
8086
10450
  Req,
10451
+ Resource,
8087
10452
  RouteParamType,
8088
10453
  RouterRegistry,
8089
10454
  ScalarPlugin,
@@ -8096,13 +10461,18 @@ export {
8096
10461
  ShokupanResponse,
8097
10462
  ShokupanRouter,
8098
10463
  Spec,
10464
+ Tool,
8099
10465
  Use,
8100
10466
  ValidationError,
10467
+ attachSocketIOBridge,
8101
10468
  compileValidators,
8102
10469
  compose,
8103
10470
  enableOpenApiValidation,
8104
10471
  openApiValidator,
8105
10472
  precompileValidators,
10473
+ serveStatic,
10474
+ traceHandler,
10475
+ traceMiddleware,
8106
10476
  useExpress,
8107
10477
  valibot,
8108
10478
  validate