shokupan 0.12.0 → 0.13.1
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 +1 -0
- package/dist/{analyzer-BkNQHWj4.js → analyzer-B0fMzeIo.js} +2 -2
- package/dist/{analyzer-BkNQHWj4.js.map → analyzer-B0fMzeIo.js.map} +1 -1
- package/dist/{analyzer-DM-OlRq8.cjs → analyzer-BOtveWL-.cjs} +2 -2
- package/dist/{analyzer-DM-OlRq8.cjs.map → analyzer-BOtveWL-.cjs.map} +1 -1
- package/dist/{analyzer.impl-CVJ8zfGQ.cjs → analyzer.impl-CUDO6vpn.cjs} +72 -5
- package/dist/analyzer.impl-CUDO6vpn.cjs.map +1 -0
- package/dist/{analyzer.impl-CsA1bS_s.js → analyzer.impl-DmHe92Oi.js} +72 -5
- package/dist/analyzer.impl-DmHe92Oi.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1849 -190
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1892 -233
- package/dist/index.js.map +1 -1
- 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 +1 -2
- 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/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 +21 -2
- package/dist/util/decorators.d.ts +38 -0
- package/dist/util/env-loader.d.ts +99 -0
- package/dist/util/mcp-protocol.d.ts +52 -0
- package/dist/util/promise.d.ts +16 -0
- package/dist/util/symbol.d.ts +4 -0
- package/dist/util/types.d.ts +10 -2
- package/package.json +36 -11
- package/dist/analyzer.impl-CVJ8zfGQ.cjs.map +0 -1
- package/dist/analyzer.impl-CsA1bS_s.js.map +0 -1
- 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,8 +19,10 @@ 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";
|
|
25
26
|
import { Readable } from "node:stream";
|
|
26
27
|
import * as zlib from "node:zlib";
|
|
27
28
|
import Ajv from "ajv";
|
|
@@ -330,6 +331,10 @@ const $cachedCookies = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedCookies");
|
|
|
330
331
|
const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
|
|
331
332
|
const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
|
|
332
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");
|
|
333
338
|
function isValidCookieDomain(domain, currentHost) {
|
|
334
339
|
const hostWithoutPort = currentHost.split(":")[0];
|
|
335
340
|
if (domain === hostWithoutPort) return true;
|
|
@@ -1098,11 +1103,11 @@ class ShokupanContext {
|
|
|
1098
1103
|
}
|
|
1099
1104
|
const compose = (middleware) => {
|
|
1100
1105
|
if (!middleware.length) {
|
|
1101
|
-
return (
|
|
1106
|
+
return (context, next) => {
|
|
1102
1107
|
return next ? next() : Promise.resolve();
|
|
1103
1108
|
};
|
|
1104
1109
|
}
|
|
1105
|
-
return function dispatch(
|
|
1110
|
+
return function dispatch(context, next) {
|
|
1106
1111
|
let index = -1;
|
|
1107
1112
|
async function runner(i) {
|
|
1108
1113
|
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
@@ -1116,31 +1121,109 @@ const compose = (middleware) => {
|
|
|
1116
1121
|
console.error(`[Middleware Error] Item at index ${i} is not a function! It is: ${typeof fn} (${name})`, fn);
|
|
1117
1122
|
throw new TypeError(`Middleware at index ${i} must be a function, got ${name}`);
|
|
1118
1123
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
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();
|
|
1121
1148
|
}
|
|
1122
|
-
const debug = context2[$debug];
|
|
1123
|
-
const debugId = fn._debugId || fn.name || "anonymous";
|
|
1124
|
-
const previousNode = debug.getCurrentNode();
|
|
1125
|
-
debug.trackEdge(previousNode, debugId);
|
|
1126
|
-
debug.setNode(debugId);
|
|
1127
|
-
const start = performance.now();
|
|
1128
1149
|
try {
|
|
1129
|
-
const res = await
|
|
1130
|
-
|
|
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
|
+
}
|
|
1131
1183
|
return res;
|
|
1132
1184
|
} catch (err) {
|
|
1133
|
-
|
|
1134
|
-
|
|
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;
|
|
1135
1218
|
} finally {
|
|
1136
|
-
if (previousNode) debug.setNode(previousNode);
|
|
1219
|
+
if (debug && previousNode) debug.setNode(previousNode);
|
|
1137
1220
|
}
|
|
1138
1221
|
}
|
|
1139
1222
|
return runner(0);
|
|
1140
1223
|
};
|
|
1141
1224
|
};
|
|
1142
1225
|
function isObject(item) {
|
|
1143
|
-
return item && typeof item === "object" && !Array.isArray(item);
|
|
1226
|
+
return !!(item && typeof item === "object" && !Array.isArray(item));
|
|
1144
1227
|
}
|
|
1145
1228
|
function deepMerge(target, ...sources) {
|
|
1146
1229
|
if (!sources.length) return target;
|
|
@@ -1405,7 +1488,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1405
1488
|
let astMiddlewareRegistry = {};
|
|
1406
1489
|
let applications = [];
|
|
1407
1490
|
try {
|
|
1408
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-
|
|
1491
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-B0fMzeIo.js");
|
|
1409
1492
|
const entrypoint = rootRouter.metadata?.file;
|
|
1410
1493
|
const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
|
|
1411
1494
|
const analysisResult = await analyzer.analyze();
|
|
@@ -1457,7 +1540,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1457
1540
|
isBuiltinPlugin = true;
|
|
1458
1541
|
pluginName = router.metadata.pluginName;
|
|
1459
1542
|
tag = pluginName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1460
|
-
} 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$/)) {
|
|
1461
1544
|
isBuiltinPlugin = true;
|
|
1462
1545
|
const match = router.metadata.file.match(/plugins\/application\/([^/]+)/);
|
|
1463
1546
|
if (match) {
|
|
@@ -1615,7 +1698,39 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1615
1698
|
const params = [];
|
|
1616
1699
|
if (astMatch.requestTypes?.query) {
|
|
1617
1700
|
for (const [name, _type] of Object.entries(astMatch.requestTypes.query)) {
|
|
1618
|
-
|
|
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" } });
|
|
1619
1734
|
}
|
|
1620
1735
|
}
|
|
1621
1736
|
if (params.length > 0) {
|
|
@@ -1631,20 +1746,20 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1631
1746
|
});
|
|
1632
1747
|
}
|
|
1633
1748
|
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
1634
|
-
let
|
|
1749
|
+
let file2;
|
|
1635
1750
|
let line;
|
|
1636
1751
|
if (route.metadata?.file) {
|
|
1637
|
-
|
|
1752
|
+
file2 = route.metadata.file;
|
|
1638
1753
|
line = route.metadata.line || 1;
|
|
1639
1754
|
}
|
|
1640
1755
|
operation["x-source-info"] = {
|
|
1641
1756
|
snippet: runtimeSource,
|
|
1642
1757
|
isRuntime: true,
|
|
1643
|
-
...
|
|
1758
|
+
...file2 ? { file: file2, line: line || 1 } : {}
|
|
1644
1759
|
};
|
|
1645
|
-
if (
|
|
1760
|
+
if (file2) {
|
|
1646
1761
|
operation["x-shokupan-source"] = {
|
|
1647
|
-
file,
|
|
1762
|
+
file: file2,
|
|
1648
1763
|
line: line || 1,
|
|
1649
1764
|
code: runtimeSource,
|
|
1650
1765
|
pluginName: route.handler.pluginName
|
|
@@ -1673,9 +1788,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1673
1788
|
const mergedParams = [...existingParams];
|
|
1674
1789
|
pathParams.forEach((p) => {
|
|
1675
1790
|
const idx = mergedParams.findIndex((ep) => ep.in === "path" && ep.name === p.name);
|
|
1676
|
-
if (idx
|
|
1677
|
-
mergedParams[idx] = deepMerge(mergedParams[idx], p);
|
|
1678
|
-
} else {
|
|
1791
|
+
if (idx === -1) {
|
|
1679
1792
|
mergedParams.push(p);
|
|
1680
1793
|
}
|
|
1681
1794
|
});
|
|
@@ -1964,6 +2077,166 @@ function serveStatic(config, prefix) {
|
|
|
1964
2077
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1965
2078
|
return serveStaticMiddleware;
|
|
1966
2079
|
}
|
|
2080
|
+
class OpenTelemetryPlugin {
|
|
2081
|
+
constructor(options = {}) {
|
|
2082
|
+
this.options = options;
|
|
2083
|
+
}
|
|
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());
|
|
2095
|
+
}
|
|
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
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
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,
|
|
2144
|
+
attributes: {
|
|
2145
|
+
"code.function": middlewareName,
|
|
2146
|
+
"component": "shokupan.middleware"
|
|
2147
|
+
}
|
|
2148
|
+
}, async (span) => {
|
|
2149
|
+
try {
|
|
2150
|
+
const result = await fn(ctx, next);
|
|
2151
|
+
return result;
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
span.recordException(err);
|
|
2154
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
2155
|
+
throw err;
|
|
2156
|
+
} finally {
|
|
2157
|
+
span.end();
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
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
|
+
}
|
|
1967
2240
|
const metadataStore = /* @__PURE__ */ new WeakMap();
|
|
1968
2241
|
function defineMetadata(key, value, target, propertyKey) {
|
|
1969
2242
|
let targetMetadata = metadataStore.get(target);
|
|
@@ -2060,31 +2333,8 @@ const di = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
|
2060
2333
|
__proto__: null,
|
|
2061
2334
|
Container
|
|
2062
2335
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
2063
|
-
const tracer = trace.getTracer("shokupan.middleware");
|
|
2064
|
-
function traceHandler(fn, name) {
|
|
2065
|
-
return async function(...args) {
|
|
2066
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
2067
|
-
kind: SpanKind.INTERNAL,
|
|
2068
|
-
attributes: {
|
|
2069
|
-
"http.route": name,
|
|
2070
|
-
"component": "shokupan.route"
|
|
2071
|
-
}
|
|
2072
|
-
}, async (span) => {
|
|
2073
|
-
try {
|
|
2074
|
-
const result = await fn.apply(this, args);
|
|
2075
|
-
return result;
|
|
2076
|
-
} catch (err) {
|
|
2077
|
-
span.recordException(err);
|
|
2078
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
2079
|
-
throw err;
|
|
2080
|
-
} finally {
|
|
2081
|
-
span.end();
|
|
2082
|
-
}
|
|
2083
|
-
});
|
|
2084
|
-
};
|
|
2085
|
-
}
|
|
2086
2336
|
function getCallerInfo(skipFrames = 1) {
|
|
2087
|
-
let
|
|
2337
|
+
let file2 = "unknown";
|
|
2088
2338
|
let line = 0;
|
|
2089
2339
|
try {
|
|
2090
2340
|
const err = new Error();
|
|
@@ -2100,19 +2350,20 @@ function getCallerInfo(skipFrames = 1) {
|
|
|
2100
2350
|
if (l.includes("src/router.ts")) continue;
|
|
2101
2351
|
if (l.includes("src/util/decorators.ts")) continue;
|
|
2102
2352
|
if (l.includes("src/shokupan.ts")) continue;
|
|
2353
|
+
if (l.includes("src/plugins/application/openapi/openapi.ts")) continue;
|
|
2103
2354
|
found++;
|
|
2104
2355
|
if (found >= skipFrames) {
|
|
2105
2356
|
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
2106
2357
|
if (match) {
|
|
2107
|
-
|
|
2358
|
+
file2 = match[1];
|
|
2108
2359
|
line = parseInt(match[2], 10);
|
|
2109
|
-
return { file, line };
|
|
2360
|
+
return { file: file2, line };
|
|
2110
2361
|
}
|
|
2111
2362
|
}
|
|
2112
2363
|
}
|
|
2113
2364
|
} catch (e) {
|
|
2114
2365
|
}
|
|
2115
|
-
return { file, line };
|
|
2366
|
+
return { file: file2, line };
|
|
2116
2367
|
}
|
|
2117
2368
|
const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
|
|
2118
2369
|
var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
@@ -2168,6 +2419,10 @@ class ControllerScanner {
|
|
|
2168
2419
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
2169
2420
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
2170
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];
|
|
2171
2426
|
let routesAttached = 0;
|
|
2172
2427
|
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
2173
2428
|
const name = Array.from(methods)[i];
|
|
@@ -2292,6 +2547,14 @@ class ControllerScanner {
|
|
|
2292
2547
|
return composed(ctx, () => wrappedHandler(ctx));
|
|
2293
2548
|
};
|
|
2294
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
|
+
}
|
|
2295
2558
|
finalHandler.originalHandler = originalHandler;
|
|
2296
2559
|
if (finalHandler !== wrappedHandler) {
|
|
2297
2560
|
wrappedHandler.originalHandler = originalHandler;
|
|
@@ -2348,6 +2611,25 @@ class ControllerScanner {
|
|
|
2348
2611
|
wrappedHandler.originalHandler = originalHandler;
|
|
2349
2612
|
router.event(eventConfig.eventName, wrappedHandler);
|
|
2350
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
|
+
}
|
|
2351
2633
|
}
|
|
2352
2634
|
if (routesAttached === 0) {
|
|
2353
2635
|
console.warn(`No routes attached to controller ${instance.constructor.name}`);
|
|
@@ -2378,71 +2660,177 @@ function getErrorStatus(err) {
|
|
|
2378
2660
|
}
|
|
2379
2661
|
return 500;
|
|
2380
2662
|
}
|
|
2663
|
+
class NotFoundError extends HttpError {
|
|
2664
|
+
constructor(message = "Not Found") {
|
|
2665
|
+
super(message, 404);
|
|
2666
|
+
this.name = "NotFoundError";
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2381
2669
|
class EventError extends HttpError {
|
|
2382
2670
|
constructor(message = "Event Error") {
|
|
2383
2671
|
super(message, 500);
|
|
2384
2672
|
this.name = "EventError";
|
|
2385
2673
|
}
|
|
2386
2674
|
}
|
|
2387
|
-
class
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
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
|
+
}
|
|
2417
2736
|
try {
|
|
2418
|
-
const
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
timestamp,
|
|
2428
|
-
duration,
|
|
2429
|
-
file,
|
|
2430
|
-
line,
|
|
2431
|
-
error: error ? String(error) : void 0,
|
|
2432
|
-
metadata: {
|
|
2433
|
-
isBuiltin,
|
|
2434
|
-
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) }]
|
|
2435
2746
|
}
|
|
2436
|
-
}
|
|
2437
|
-
} catch (err) {
|
|
2747
|
+
};
|
|
2438
2748
|
}
|
|
2439
|
-
}
|
|
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");
|
|
2440
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 }
|
|
2441
2811
|
};
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
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;
|
|
2446
2834
|
}
|
|
2447
2835
|
}
|
|
2448
2836
|
class ShokupanRequestBase {
|
|
@@ -2656,6 +3044,7 @@ class ShokupanRouter {
|
|
|
2656
3044
|
trie = new RouterTrie();
|
|
2657
3045
|
metadata;
|
|
2658
3046
|
// Metadata for the router itself
|
|
3047
|
+
mcpProtocol = new McpProtocol();
|
|
2659
3048
|
currentGuards = [];
|
|
2660
3049
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
2661
3050
|
/**
|
|
@@ -2781,6 +3170,39 @@ class ShokupanRouter {
|
|
|
2781
3170
|
handlers.push(handler);
|
|
2782
3171
|
return this;
|
|
2783
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
|
+
}
|
|
2784
3206
|
/**
|
|
2785
3207
|
* Finds an event handler(s) by name.
|
|
2786
3208
|
*/
|
|
@@ -3079,63 +3501,52 @@ class ShokupanRouter {
|
|
|
3079
3501
|
}
|
|
3080
3502
|
}
|
|
3081
3503
|
}
|
|
3082
|
-
let wrappedHandler = async (ctx) => {
|
|
3083
|
-
return handler(ctx);
|
|
3084
|
-
};
|
|
3085
|
-
wrappedHandler.originalHandler = handler.originalHandler || handler;
|
|
3086
|
-
const routeGuards = [...this.currentGuards];
|
|
3087
3504
|
const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
|
|
3088
|
-
|
|
3089
|
-
|
|
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;
|
|
3090
3510
|
wrappedHandler = async (ctx) => {
|
|
3091
|
-
if (ctx.server) {
|
|
3511
|
+
if (effectiveTimeout && effectiveTimeout > 0 && ctx.server) {
|
|
3092
3512
|
ctx.server.timeout(ctx.req, effectiveTimeout / 1e3);
|
|
3093
3513
|
}
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
}
|
|
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) {
|
|
3116
3539
|
return ctx.json({ error: "Forbidden" }, 403);
|
|
3117
3540
|
}
|
|
3118
|
-
} catch (error) {
|
|
3119
|
-
throw error;
|
|
3120
|
-
}
|
|
3121
|
-
if (!guardPassed) {
|
|
3122
|
-
return ctx.json({ error: "Forbidden" }, 403);
|
|
3123
3541
|
}
|
|
3124
3542
|
}
|
|
3125
|
-
return
|
|
3126
|
-
};
|
|
3127
|
-
}
|
|
3128
|
-
const effectiveRenderer = renderer ?? this.config?.renderer ?? this.rootConfig?.renderer;
|
|
3129
|
-
if (effectiveRenderer) {
|
|
3130
|
-
const innerHandler = wrappedHandler;
|
|
3131
|
-
wrappedHandler = async (ctx) => {
|
|
3132
|
-
ctx.setRenderer(effectiveRenderer);
|
|
3133
|
-
return innerHandler(ctx);
|
|
3543
|
+
return originalHandler(ctx);
|
|
3134
3544
|
};
|
|
3545
|
+
wrappedHandler.originalHandler = handler.originalHandler || handler;
|
|
3135
3546
|
}
|
|
3136
|
-
const { file, line } = metadata || getCallerInfo();
|
|
3547
|
+
const { file: file2, line } = metadata || getCallerInfo();
|
|
3137
3548
|
wrappedHandler = MiddlewareTracker.wrap(wrappedHandler, {
|
|
3138
|
-
file,
|
|
3549
|
+
file: file2,
|
|
3139
3550
|
line,
|
|
3140
3551
|
name: handler.name || "anonymous",
|
|
3141
3552
|
isBuiltin: handler.isBuiltin,
|
|
@@ -3158,7 +3569,7 @@ class ShokupanRouter {
|
|
|
3158
3569
|
requestTimeout,
|
|
3159
3570
|
renderer,
|
|
3160
3571
|
metadata: {
|
|
3161
|
-
file,
|
|
3572
|
+
file: file2,
|
|
3162
3573
|
line
|
|
3163
3574
|
},
|
|
3164
3575
|
controller,
|
|
@@ -3198,7 +3609,7 @@ class ShokupanRouter {
|
|
|
3198
3609
|
guard(specOrHandler, handler) {
|
|
3199
3610
|
const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
|
|
3200
3611
|
const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
|
|
3201
|
-
let
|
|
3612
|
+
let file2 = "unknown";
|
|
3202
3613
|
let line = 0;
|
|
3203
3614
|
try {
|
|
3204
3615
|
const err = new Error();
|
|
@@ -3209,14 +3620,14 @@ class ShokupanRouter {
|
|
|
3209
3620
|
if (callerLine) {
|
|
3210
3621
|
const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
|
|
3211
3622
|
if (match) {
|
|
3212
|
-
|
|
3623
|
+
file2 = match[1];
|
|
3213
3624
|
line = parseInt(match[2], 10);
|
|
3214
3625
|
}
|
|
3215
3626
|
}
|
|
3216
3627
|
} catch (e) {
|
|
3217
3628
|
}
|
|
3218
3629
|
const trackedGuard = MiddlewareTracker.wrap(guardHandler, {
|
|
3219
|
-
file,
|
|
3630
|
+
file: file2,
|
|
3220
3631
|
line,
|
|
3221
3632
|
name: guardHandler.name || "guard"
|
|
3222
3633
|
});
|
|
@@ -3662,10 +4073,10 @@ class DefaultFileSystemAdapter {
|
|
|
3662
4073
|
}
|
|
3663
4074
|
async stat(path) {
|
|
3664
4075
|
if (typeof Bun !== "undefined") {
|
|
3665
|
-
const
|
|
4076
|
+
const file2 = Bun.file(path);
|
|
3666
4077
|
return {
|
|
3667
|
-
size:
|
|
3668
|
-
mtime: new Date(
|
|
4078
|
+
size: file2.size,
|
|
4079
|
+
mtime: new Date(file2.lastModified)
|
|
3669
4080
|
};
|
|
3670
4081
|
} else {
|
|
3671
4082
|
fs ??= await import("node:fs/promises");
|
|
@@ -3832,6 +4243,32 @@ class SurrealDatastore {
|
|
|
3832
4243
|
return this.db.close();
|
|
3833
4244
|
}
|
|
3834
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
|
+
}
|
|
3835
4272
|
const defaults = {
|
|
3836
4273
|
port: 3e3,
|
|
3837
4274
|
hostname: "localhost",
|
|
@@ -3869,21 +4306,38 @@ class Shokupan extends ShokupanRouter {
|
|
|
3869
4306
|
this[$isApplication] = true;
|
|
3870
4307
|
this[$appRoot] = this;
|
|
3871
4308
|
this.applicationConfig = config;
|
|
3872
|
-
const { file, line } = getCallerInfo();
|
|
4309
|
+
const { file: file2, line } = getCallerInfo();
|
|
3873
4310
|
this.metadata = {
|
|
3874
|
-
file,
|
|
4311
|
+
file: file2,
|
|
3875
4312
|
line,
|
|
3876
4313
|
name: "ShokupanApplication"
|
|
3877
4314
|
};
|
|
3878
|
-
if (this.applicationConfig.
|
|
4315
|
+
if (this.applicationConfig.defaultSecurityHeaders) {
|
|
3879
4316
|
const { SecurityHeaders: SecurityHeaders2 } = require("./plugins/middleware/security-headers");
|
|
3880
|
-
this.use(SecurityHeaders2(this.applicationConfig.
|
|
4317
|
+
this.use(SecurityHeaders2(this.applicationConfig.defaultSecurityHeaders === true ? {} : this.applicationConfig.defaultSecurityHeaders));
|
|
3881
4318
|
}
|
|
3882
4319
|
if (this.applicationConfig.adapter !== "wintercg") {
|
|
3883
4320
|
this.dbPromise = this.initDatastore().catch((err) => {
|
|
3884
4321
|
this.logger?.debug("Failed to initialize default datastore", { error: err });
|
|
3885
4322
|
});
|
|
3886
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
|
+
}
|
|
3887
4341
|
}
|
|
3888
4342
|
async initDatastore() {
|
|
3889
4343
|
let engines = this.applicationConfig.surreal?.engines;
|
|
@@ -3913,9 +4367,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
3913
4367
|
* Adds middleware to the application.
|
|
3914
4368
|
*/
|
|
3915
4369
|
use(middleware) {
|
|
3916
|
-
const { file, line } = getCallerInfo();
|
|
4370
|
+
const { file: file2, line } = getCallerInfo();
|
|
3917
4371
|
const wrapped = MiddlewareTracker.wrap(middleware, {
|
|
3918
|
-
file,
|
|
4372
|
+
file: file2,
|
|
3919
4373
|
line,
|
|
3920
4374
|
name: middleware.name || "middleware",
|
|
3921
4375
|
isBuiltin: middleware.isBuiltin,
|
|
@@ -3969,6 +4423,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
3969
4423
|
this.get("/.well-known/openapi.yaml", async (ctx) => {
|
|
3970
4424
|
try {
|
|
3971
4425
|
await this.openApiSpecPromise;
|
|
4426
|
+
const { dump } = await import("js-yaml");
|
|
3972
4427
|
const yaml = dump(this.openApiSpec);
|
|
3973
4428
|
return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
|
|
3974
4429
|
} catch (e) {
|
|
@@ -4160,7 +4615,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
4160
4615
|
*/
|
|
4161
4616
|
async fetch(req, server) {
|
|
4162
4617
|
if (this.applicationConfig.enableTracing) {
|
|
4163
|
-
const
|
|
4618
|
+
const { trace, context } = await import("@opentelemetry/api");
|
|
4619
|
+
const tracer = trace.getTracer("shokupan.application");
|
|
4164
4620
|
const store = asyncContext.getStore();
|
|
4165
4621
|
const attrs = {
|
|
4166
4622
|
attributes: {
|
|
@@ -4170,7 +4626,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
4170
4626
|
};
|
|
4171
4627
|
const parent = store?.span;
|
|
4172
4628
|
const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
|
|
4173
|
-
return
|
|
4629
|
+
return tracer.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
|
|
4174
4630
|
const ctxStore = new RequestContextStore();
|
|
4175
4631
|
ctxStore.span = span;
|
|
4176
4632
|
ctxStore.request = req;
|
|
@@ -4178,16 +4634,19 @@ class Shokupan extends ShokupanRouter {
|
|
|
4178
4634
|
});
|
|
4179
4635
|
}
|
|
4180
4636
|
if (this.applicationConfig.enableAsyncLocalStorage) {
|
|
4637
|
+
const requestId = this.applicationConfig.idGenerator?.() ?? nanoid();
|
|
4181
4638
|
const ctxStore = new RequestContextStore();
|
|
4182
4639
|
ctxStore.request = req;
|
|
4183
|
-
|
|
4640
|
+
ctxStore["requestId"] = requestId;
|
|
4641
|
+
ctxStore["app"] = this;
|
|
4642
|
+
return asyncContext.run(ctxStore, () => this.handleRequest(req, server, requestId));
|
|
4184
4643
|
}
|
|
4185
4644
|
return this.handleRequest(req, server);
|
|
4186
4645
|
}
|
|
4187
|
-
async handleRequest(req, server) {
|
|
4646
|
+
async handleRequest(req, server, requestId) {
|
|
4188
4647
|
const request = req;
|
|
4189
4648
|
const controller = new AbortController();
|
|
4190
|
-
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);
|
|
4191
4650
|
const handle = async () => {
|
|
4192
4651
|
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
4193
4652
|
const msg = "Too Many Requests (CPU Backpressure)";
|
|
@@ -4208,15 +4667,97 @@ class Shokupan extends ShokupanRouter {
|
|
|
4208
4667
|
ctx[$routeMatched] = true;
|
|
4209
4668
|
ctx.params = match.params;
|
|
4210
4669
|
if (bodyParsing) await bodyParsing;
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
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
|
+
}
|
|
4746
|
+
return match.handler(ctx);
|
|
4747
|
+
}
|
|
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();
|
|
4755
|
+
});
|
|
4756
|
+
let response;
|
|
4757
|
+
if (result instanceof Response) {
|
|
4758
|
+
response = result;
|
|
4759
|
+
} else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
|
|
4760
|
+
response = ctx[$finalResponse];
|
|
4220
4761
|
} else if (result === null || result === void 0) {
|
|
4221
4762
|
if (ctx[$finalResponse] instanceof Response) {
|
|
4222
4763
|
response = ctx[$finalResponse];
|
|
@@ -4229,14 +4770,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
4229
4770
|
}
|
|
4230
4771
|
response = ctx.send(null, { status, headers: ctx.response.headers });
|
|
4231
4772
|
} else {
|
|
4232
|
-
|
|
4233
|
-
return void 0;
|
|
4234
|
-
}
|
|
4235
|
-
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
4236
|
-
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
4237
|
-
} else {
|
|
4238
|
-
response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
|
|
4239
|
-
}
|
|
4773
|
+
throw new NotFoundError();
|
|
4240
4774
|
}
|
|
4241
4775
|
} else if (typeof result === "object") {
|
|
4242
4776
|
response = ctx.json(result);
|
|
@@ -4603,6 +5137,37 @@ function Event(eventName) {
|
|
|
4603
5137
|
function RateLimit(options) {
|
|
4604
5138
|
return Use(RateLimitMiddleware(options));
|
|
4605
5139
|
}
|
|
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
|
+
}
|
|
4606
5171
|
function ApiExplorerApp({ spec, base, asyncSpec, config }) {
|
|
4607
5172
|
const hierarchy = /* @__PURE__ */ new Map();
|
|
4608
5173
|
const addRoute = (groupKey, route) => {
|
|
@@ -4970,8 +5535,8 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
4970
5535
|
return dir;
|
|
4971
5536
|
}
|
|
4972
5537
|
init() {
|
|
4973
|
-
const serveFile = async (ctx,
|
|
4974
|
-
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");
|
|
4975
5540
|
ctx.set("Content-Type", type);
|
|
4976
5541
|
return ctx.send(content);
|
|
4977
5542
|
};
|
|
@@ -4994,11 +5559,11 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
4994
5559
|
this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
|
|
4995
5560
|
this.get("/explorer-client.mjs", (ctx) => serveFile(ctx, "explorer-client.mjs", "application/javascript"));
|
|
4996
5561
|
this.get("/_source", async (ctx) => {
|
|
4997
|
-
const
|
|
4998
|
-
if (!
|
|
5562
|
+
const file2 = ctx.query["file"];
|
|
5563
|
+
if (!file2) return ctx.text("Missing file parameter", 400);
|
|
4999
5564
|
const { resolve: resolve2, normalize, isAbsolute } = await import("node:path");
|
|
5000
5565
|
const cwd = process.cwd();
|
|
5001
|
-
const resolvedPath = resolve2(cwd,
|
|
5566
|
+
const resolvedPath = resolve2(cwd, file2);
|
|
5002
5567
|
if (!resolvedPath.startsWith(cwd)) {
|
|
5003
5568
|
return ctx.text("Forbidden: File must be within project root", 403);
|
|
5004
5569
|
}
|
|
@@ -5234,7 +5799,7 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
5234
5799
|
let astMiddlewareRegistry = {};
|
|
5235
5800
|
let applications = [];
|
|
5236
5801
|
try {
|
|
5237
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-
|
|
5802
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-B0fMzeIo.js");
|
|
5238
5803
|
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
5239
5804
|
const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
|
|
5240
5805
|
const analysisResult = await analyzer.analyze();
|
|
@@ -5607,8 +6172,8 @@ class AsyncApiPlugin extends ShokupanRouter {
|
|
|
5607
6172
|
}
|
|
5608
6173
|
}
|
|
5609
6174
|
init() {
|
|
5610
|
-
const serveFile = async (ctx,
|
|
5611
|
-
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");
|
|
5612
6177
|
ctx.set("Content-Type", type);
|
|
5613
6178
|
return ctx.send(content);
|
|
5614
6179
|
};
|
|
@@ -5640,13 +6205,13 @@ class AsyncApiPlugin extends ShokupanRouter {
|
|
|
5640
6205
|
return ctx.json(spec);
|
|
5641
6206
|
});
|
|
5642
6207
|
this.get("/_code", async (ctx) => {
|
|
5643
|
-
const
|
|
5644
|
-
if (!
|
|
6208
|
+
const file2 = ctx.query["file"];
|
|
6209
|
+
if (!file2 || typeof file2 !== "string") {
|
|
5645
6210
|
return ctx.text("Missing file parameter", 400);
|
|
5646
6211
|
}
|
|
5647
6212
|
const { resolve: resolve2 } = await import("node:path");
|
|
5648
6213
|
const cwd = process.cwd();
|
|
5649
|
-
const resolvedPath = resolve2(cwd,
|
|
6214
|
+
const resolvedPath = resolve2(cwd, file2);
|
|
5650
6215
|
if (!resolvedPath.startsWith(cwd)) {
|
|
5651
6216
|
return ctx.text("Forbidden: File must be within project root", 403);
|
|
5652
6217
|
}
|
|
@@ -7506,6 +8071,463 @@ class Dashboard {
|
|
|
7506
8071
|
function unknownError(ctx) {
|
|
7507
8072
|
return ctx.json({ error: "Unknown Error" }, 500);
|
|
7508
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
|
+
}
|
|
7509
8531
|
class GraphQLApolloPlugin extends ShokupanRouter {
|
|
7510
8532
|
// Use generic any or verify type
|
|
7511
8533
|
constructor(pluginOptions) {
|
|
@@ -7580,6 +8602,377 @@ class GraphQLApolloPlugin extends ShokupanRouter {
|
|
|
7580
8602
|
});
|
|
7581
8603
|
}
|
|
7582
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
|
+
}
|
|
7583
8976
|
class ScalarPlugin extends ShokupanRouter {
|
|
7584
8977
|
constructor(pluginOptions = {}) {
|
|
7585
8978
|
pluginOptions.config ??= {};
|
|
@@ -7755,7 +9148,7 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
7755
9148
|
try {
|
|
7756
9149
|
const entrypoint = process.argv[1];
|
|
7757
9150
|
console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
|
|
7758
|
-
const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
9151
|
+
const analyzer = new OpenAPIAnalyzer$1(process.cwd(), entrypoint);
|
|
7759
9152
|
let staticSpec = await analyzer.analyze();
|
|
7760
9153
|
if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
|
|
7761
9154
|
deepMerge(this.pluginOptions.baseDocument, staticSpec);
|
|
@@ -7768,6 +9161,79 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
7768
9161
|
}
|
|
7769
9162
|
}
|
|
7770
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
|
+
}
|
|
7771
9237
|
function createLimitStream(maxSize) {
|
|
7772
9238
|
let size = 0;
|
|
7773
9239
|
return new TransformStream({
|
|
@@ -8410,6 +9876,168 @@ function enableOpenApiValidation(app) {
|
|
|
8410
9876
|
precompileValidators(app, spec);
|
|
8411
9877
|
});
|
|
8412
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
|
+
}
|
|
8413
10041
|
function SecurityHeaders(options = {}) {
|
|
8414
10042
|
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
8415
10043
|
const set = (k, v) => ctx.response.set(k, v);
|
|
@@ -8622,43 +10250,56 @@ function Session(options) {
|
|
|
8622
10250
|
}
|
|
8623
10251
|
const sessObj = existing;
|
|
8624
10252
|
Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
|
|
8625
|
-
sessObj.save = (
|
|
8626
|
-
|
|
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
|
+
});
|
|
8627
10260
|
};
|
|
8628
|
-
sessObj.destroy = (
|
|
8629
|
-
|
|
8630
|
-
|
|
10261
|
+
sessObj.destroy = () => {
|
|
10262
|
+
return new Promise((resolve2, reject) => {
|
|
10263
|
+
store.destroy(sessObj.id, (err) => {
|
|
10264
|
+
if (err) reject(err);
|
|
10265
|
+
else resolve2();
|
|
10266
|
+
});
|
|
8631
10267
|
});
|
|
8632
10268
|
};
|
|
8633
|
-
sessObj.regenerate = (
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
const
|
|
8639
|
-
|
|
8640
|
-
|
|
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
|
+
}
|
|
8641
10280
|
}
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
10281
|
+
Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
|
|
10282
|
+
resolve2();
|
|
10283
|
+
});
|
|
8645
10284
|
});
|
|
8646
10285
|
};
|
|
8647
10286
|
sessObj.undefined = () => {
|
|
8648
10287
|
};
|
|
8649
|
-
sessObj.reload = (
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
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
|
+
}
|
|
8658
10299
|
}
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
10300
|
+
Object.assign(sessObj, sess2);
|
|
10301
|
+
resolve2();
|
|
10302
|
+
});
|
|
8662
10303
|
});
|
|
8663
10304
|
};
|
|
8664
10305
|
sessObj.touch = () => {
|
|
@@ -8751,11 +10392,15 @@ export {
|
|
|
8751
10392
|
$isApplication,
|
|
8752
10393
|
$isMounted,
|
|
8753
10394
|
$isRouter,
|
|
10395
|
+
$mcpPrompts,
|
|
10396
|
+
$mcpResources,
|
|
10397
|
+
$mcpTools,
|
|
8754
10398
|
$middleware,
|
|
8755
10399
|
$mountPath,
|
|
8756
10400
|
$parent,
|
|
8757
10401
|
$rawBody,
|
|
8758
10402
|
$requestId,
|
|
10403
|
+
$resilienceConfig,
|
|
8759
10404
|
$routeArgs,
|
|
8760
10405
|
$routeMatched,
|
|
8761
10406
|
$routeMethods,
|
|
@@ -8777,24 +10422,33 @@ export {
|
|
|
8777
10422
|
Ctx,
|
|
8778
10423
|
Dashboard,
|
|
8779
10424
|
Delete,
|
|
10425
|
+
ErrorView,
|
|
8780
10426
|
Event,
|
|
8781
10427
|
Get,
|
|
8782
10428
|
GraphQLApolloPlugin,
|
|
10429
|
+
GraphQLYogaPlugin,
|
|
8783
10430
|
HTTPMethods,
|
|
8784
10431
|
Head,
|
|
8785
10432
|
Headers$1 as Headers,
|
|
10433
|
+
HtmxPlugin,
|
|
10434
|
+
Idempotency,
|
|
8786
10435
|
Inject,
|
|
8787
10436
|
Injectable,
|
|
10437
|
+
MCPServerPlugin,
|
|
8788
10438
|
MemoryStore,
|
|
10439
|
+
OpenTelemetryPlugin,
|
|
8789
10440
|
Options,
|
|
8790
10441
|
Param,
|
|
8791
10442
|
Patch,
|
|
8792
10443
|
Post,
|
|
10444
|
+
Prompt,
|
|
10445
|
+
Proxy$1 as Proxy,
|
|
8793
10446
|
Put,
|
|
8794
10447
|
Query,
|
|
8795
10448
|
RateLimit,
|
|
8796
10449
|
RateLimitMiddleware,
|
|
8797
10450
|
Req,
|
|
10451
|
+
Resource,
|
|
8798
10452
|
RouteParamType,
|
|
8799
10453
|
RouterRegistry,
|
|
8800
10454
|
ScalarPlugin,
|
|
@@ -8807,13 +10461,18 @@ export {
|
|
|
8807
10461
|
ShokupanResponse,
|
|
8808
10462
|
ShokupanRouter,
|
|
8809
10463
|
Spec,
|
|
10464
|
+
Tool,
|
|
8810
10465
|
Use,
|
|
8811
10466
|
ValidationError,
|
|
10467
|
+
attachSocketIOBridge,
|
|
8812
10468
|
compileValidators,
|
|
8813
10469
|
compose,
|
|
8814
10470
|
enableOpenApiValidation,
|
|
8815
10471
|
openApiValidator,
|
|
8816
10472
|
precompileValidators,
|
|
10473
|
+
serveStatic,
|
|
10474
|
+
traceHandler,
|
|
10475
|
+
traceMiddleware,
|
|
8817
10476
|
useExpress,
|
|
8818
10477
|
valibot,
|
|
8819
10478
|
validate
|