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