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