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.cjs
CHANGED
|
@@ -25,12 +25,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
25
25
|
const nanoid = require("nanoid");
|
|
26
26
|
const promises = require("node:fs/promises");
|
|
27
27
|
const node_util = require("node:util");
|
|
28
|
+
const surrealdb = require("surrealdb");
|
|
28
29
|
const eta$1 = require("eta");
|
|
29
30
|
const promises$1 = require("fs/promises");
|
|
30
31
|
const path = require("path");
|
|
31
|
-
const
|
|
32
|
-
const surrealdb = require("surrealdb");
|
|
33
|
-
const jsYaml = require("js-yaml");
|
|
32
|
+
const cockatiel = require("cockatiel");
|
|
34
33
|
const http$1 = require("node:http");
|
|
35
34
|
require("node:https");
|
|
36
35
|
const node_async_hooks = require("node:async_hooks");
|
|
@@ -43,8 +42,10 @@ const net = require("node:net");
|
|
|
43
42
|
const os = require("node:os");
|
|
44
43
|
const node_module = require("node:module");
|
|
45
44
|
const node_perf_hooks = require("node:perf_hooks");
|
|
45
|
+
const bun = require("bun");
|
|
46
|
+
const analyzer_impl = require("./analyzer.impl-CUDO6vpn.cjs");
|
|
46
47
|
const fs$1 = require("node:fs");
|
|
47
|
-
const analyzer = require("./analyzer-
|
|
48
|
+
const analyzer = require("./analyzer-BOtveWL-.cjs");
|
|
48
49
|
const node_stream = require("node:stream");
|
|
49
50
|
const zlib = require("node:zlib");
|
|
50
51
|
const Ajv = require("ajv");
|
|
@@ -373,6 +374,10 @@ const $cachedCookies = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedCookies");
|
|
|
373
374
|
const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
|
|
374
375
|
const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
|
|
375
376
|
const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
|
|
377
|
+
const $mcpTools = /* @__PURE__ */ Symbol.for("Shokupan.mcp.tools");
|
|
378
|
+
const $mcpPrompts = /* @__PURE__ */ Symbol.for("Shokupan.mcp.prompts");
|
|
379
|
+
const $mcpResources = /* @__PURE__ */ Symbol.for("Shokupan.mcp.resources");
|
|
380
|
+
const $resilienceConfig = /* @__PURE__ */ Symbol.for("Shokupan.resilience.config");
|
|
376
381
|
function isValidCookieDomain(domain, currentHost) {
|
|
377
382
|
const hostWithoutPort = currentHost.split(":")[0];
|
|
378
383
|
if (domain === hostWithoutPort) return true;
|
|
@@ -1159,31 +1164,109 @@ const compose = (middleware) => {
|
|
|
1159
1164
|
console.error(`[Middleware Error] Item at index ${i} is not a function! It is: ${typeof fn} (${name})`, fn);
|
|
1160
1165
|
throw new TypeError(`Middleware at index ${i} must be a function, got ${name}`);
|
|
1161
1166
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1167
|
+
const trackingEnabled = context.app?.applicationConfig?.enableMiddlewareTracking;
|
|
1168
|
+
const meta = fn.metadata;
|
|
1169
|
+
let trackingStartTime = 0;
|
|
1170
|
+
if (trackingEnabled && meta) {
|
|
1171
|
+
trackingStartTime = performance.now();
|
|
1172
|
+
context.handlerStack.push({
|
|
1173
|
+
name: meta.name || fn.name || "anonymous",
|
|
1174
|
+
file: meta.file,
|
|
1175
|
+
line: meta.line,
|
|
1176
|
+
isBuiltin: meta.isBuiltin,
|
|
1177
|
+
startTime: trackingStartTime,
|
|
1178
|
+
duration: -1
|
|
1179
|
+
});
|
|
1164
1180
|
}
|
|
1165
1181
|
const debug = context[$debug];
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
debug
|
|
1170
|
-
|
|
1182
|
+
let debugId;
|
|
1183
|
+
let previousNode;
|
|
1184
|
+
let debugStart = 0;
|
|
1185
|
+
if (debug) {
|
|
1186
|
+
debugId = fn._debugId || fn.name || "anonymous";
|
|
1187
|
+
previousNode = debug.getCurrentNode();
|
|
1188
|
+
debug.trackEdge(previousNode, debugId);
|
|
1189
|
+
debug.setNode(debugId);
|
|
1190
|
+
debugStart = performance.now();
|
|
1191
|
+
}
|
|
1171
1192
|
try {
|
|
1172
|
-
const res = await
|
|
1173
|
-
|
|
1193
|
+
const res = await fn(context, () => runner(i + 1));
|
|
1194
|
+
if (trackingEnabled && meta) {
|
|
1195
|
+
const duration = performance.now() - trackingStartTime;
|
|
1196
|
+
const stackItem = context.handlerStack[context.handlerStack.length - 1];
|
|
1197
|
+
if (stackItem) stackItem.duration = duration;
|
|
1198
|
+
Promise.resolve().then(async () => {
|
|
1199
|
+
try {
|
|
1200
|
+
const db = context.app?.db;
|
|
1201
|
+
if (!db) return;
|
|
1202
|
+
const timestamp = Date.now();
|
|
1203
|
+
await db.upsert(new surrealdb.RecordId("middleware_tracking", {
|
|
1204
|
+
timestamp,
|
|
1205
|
+
name: meta.name
|
|
1206
|
+
}), {
|
|
1207
|
+
name: meta.name,
|
|
1208
|
+
path: context.path,
|
|
1209
|
+
timestamp,
|
|
1210
|
+
duration,
|
|
1211
|
+
file: meta.file,
|
|
1212
|
+
line: meta.line,
|
|
1213
|
+
error: void 0,
|
|
1214
|
+
metadata: {
|
|
1215
|
+
isBuiltin: meta.isBuiltin,
|
|
1216
|
+
pluginName: meta.pluginName
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
} catch (e) {
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
if (debug) {
|
|
1224
|
+
debug.trackStep(debugId, "middleware", performance.now() - debugStart, "success");
|
|
1225
|
+
}
|
|
1174
1226
|
return res;
|
|
1175
1227
|
} catch (err) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1228
|
+
if (trackingEnabled && meta) {
|
|
1229
|
+
const duration = performance.now() - trackingStartTime;
|
|
1230
|
+
const stackItem = context.handlerStack[context.handlerStack.length - 1];
|
|
1231
|
+
if (stackItem) stackItem.duration = duration;
|
|
1232
|
+
Promise.resolve().then(async () => {
|
|
1233
|
+
try {
|
|
1234
|
+
const db = context.app?.db;
|
|
1235
|
+
if (!db) return;
|
|
1236
|
+
const timestamp = Date.now();
|
|
1237
|
+
await db.upsert(new surrealdb.RecordId("middleware_tracking", {
|
|
1238
|
+
timestamp,
|
|
1239
|
+
name: meta.name
|
|
1240
|
+
}), {
|
|
1241
|
+
name: meta.name,
|
|
1242
|
+
path: context.path,
|
|
1243
|
+
timestamp,
|
|
1244
|
+
duration,
|
|
1245
|
+
file: meta.file,
|
|
1246
|
+
line: meta.line,
|
|
1247
|
+
error: String(err),
|
|
1248
|
+
metadata: {
|
|
1249
|
+
isBuiltin: meta.isBuiltin,
|
|
1250
|
+
pluginName: meta.pluginName
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
} catch (e) {
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
if (debug) {
|
|
1258
|
+
debug.trackStep(debugId, "middleware", performance.now() - debugStart, "error", err);
|
|
1259
|
+
}
|
|
1260
|
+
throw err;
|
|
1178
1261
|
} finally {
|
|
1179
|
-
if (previousNode) debug.setNode(previousNode);
|
|
1262
|
+
if (debug && previousNode) debug.setNode(previousNode);
|
|
1180
1263
|
}
|
|
1181
1264
|
}
|
|
1182
1265
|
return runner(0);
|
|
1183
1266
|
};
|
|
1184
1267
|
};
|
|
1185
1268
|
function isObject(item) {
|
|
1186
|
-
return item && typeof item === "object" && !Array.isArray(item);
|
|
1269
|
+
return !!(item && typeof item === "object" && !Array.isArray(item));
|
|
1187
1270
|
}
|
|
1188
1271
|
function deepMerge(target, ...sources) {
|
|
1189
1272
|
if (!sources.length) return target;
|
|
@@ -1448,7 +1531,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1448
1531
|
let astMiddlewareRegistry = {};
|
|
1449
1532
|
let applications = [];
|
|
1450
1533
|
try {
|
|
1451
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-
|
|
1534
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BOtveWL-.cjs"));
|
|
1452
1535
|
const entrypoint = rootRouter.metadata?.file;
|
|
1453
1536
|
const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
1454
1537
|
const analysisResult = await analyzer2.analyze();
|
|
@@ -1500,7 +1583,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1500
1583
|
isBuiltinPlugin = true;
|
|
1501
1584
|
pluginName = router.metadata.pluginName;
|
|
1502
1585
|
tag = pluginName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1503
|
-
} else if (router.metadata?.file && router.metadata.file.includes("plugins/application/")) {
|
|
1586
|
+
} else if (router.metadata?.file && router.metadata.file.includes("plugins/application/") && !router.metadata.file.match(/\.(spec|test)\.ts$/)) {
|
|
1504
1587
|
isBuiltinPlugin = true;
|
|
1505
1588
|
const match = router.metadata.file.match(/plugins\/application\/([^/]+)/);
|
|
1506
1589
|
if (match) {
|
|
@@ -1658,7 +1741,39 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1658
1741
|
const params = [];
|
|
1659
1742
|
if (astMatch.requestTypes?.query) {
|
|
1660
1743
|
for (const [name, _type] of Object.entries(astMatch.requestTypes.query)) {
|
|
1661
|
-
|
|
1744
|
+
let type = "string";
|
|
1745
|
+
let format;
|
|
1746
|
+
if (_type === "integer") {
|
|
1747
|
+
type = "integer";
|
|
1748
|
+
format = "int32";
|
|
1749
|
+
} else if (_type === "number") {
|
|
1750
|
+
type = "number";
|
|
1751
|
+
format = "float";
|
|
1752
|
+
} else if (_type === "boolean") type = "boolean";
|
|
1753
|
+
const schema = { type };
|
|
1754
|
+
if (format) schema.format = format;
|
|
1755
|
+
params.push({ name, in: "query", schema });
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (astMatch.requestTypes?.params) {
|
|
1759
|
+
for (const [name, _type] of Object.entries(astMatch.requestTypes.params)) {
|
|
1760
|
+
let type = "string";
|
|
1761
|
+
let format;
|
|
1762
|
+
if (_type === "integer") {
|
|
1763
|
+
type = "integer";
|
|
1764
|
+
format = "int32";
|
|
1765
|
+
} else if (_type === "number") {
|
|
1766
|
+
type = "number";
|
|
1767
|
+
format = "float";
|
|
1768
|
+
} else if (_type === "boolean") type = "boolean";
|
|
1769
|
+
const schema = { type };
|
|
1770
|
+
if (format) schema.format = format;
|
|
1771
|
+
params.push({ name, in: "path", required: true, schema });
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (astMatch.requestTypes?.headers) {
|
|
1775
|
+
for (const [name, _type] of Object.entries(astMatch.requestTypes.headers)) {
|
|
1776
|
+
params.push({ name, in: "header", schema: { type: "string" } });
|
|
1662
1777
|
}
|
|
1663
1778
|
}
|
|
1664
1779
|
if (params.length > 0) {
|
|
@@ -1716,9 +1831,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1716
1831
|
const mergedParams = [...existingParams];
|
|
1717
1832
|
pathParams.forEach((p) => {
|
|
1718
1833
|
const idx = mergedParams.findIndex((ep) => ep.in === "path" && ep.name === p.name);
|
|
1719
|
-
if (idx
|
|
1720
|
-
mergedParams[idx] = deepMerge(mergedParams[idx], p);
|
|
1721
|
-
} else {
|
|
1834
|
+
if (idx === -1) {
|
|
1722
1835
|
mergedParams.push(p);
|
|
1723
1836
|
}
|
|
1724
1837
|
});
|
|
@@ -2007,6 +2120,166 @@ function serveStatic(config, prefix) {
|
|
|
2007
2120
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
2008
2121
|
return serveStaticMiddleware;
|
|
2009
2122
|
}
|
|
2123
|
+
class OpenTelemetryPlugin {
|
|
2124
|
+
constructor(options = {}) {
|
|
2125
|
+
this.options = options;
|
|
2126
|
+
}
|
|
2127
|
+
api;
|
|
2128
|
+
sdk;
|
|
2129
|
+
async onInit(app) {
|
|
2130
|
+
try {
|
|
2131
|
+
this.api = await import("@opentelemetry/api");
|
|
2132
|
+
} catch (e) {
|
|
2133
|
+
console.warn("OpenTelemetry API not found. OpenTelemetryPlugin will be disabled.");
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
if (this.options.enableAutoInstrumentation !== false) {
|
|
2137
|
+
app.use(this.middleware());
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
middleware() {
|
|
2141
|
+
return async (ctx, next) => {
|
|
2142
|
+
if (!this.api) return next();
|
|
2143
|
+
const tracer = this.api.trace.getTracer("shokupan");
|
|
2144
|
+
return tracer.startActiveSpan(`${ctx.req.method} ${ctx.req.path}`, {
|
|
2145
|
+
kind: this.api.SpanKind.SERVER,
|
|
2146
|
+
attributes: {
|
|
2147
|
+
"http.method": ctx.req.method,
|
|
2148
|
+
"http.url": ctx.req.url,
|
|
2149
|
+
"http.host": ctx.req.host,
|
|
2150
|
+
"http.user_agent": ctx.req.headers.get("user-agent") || void 0
|
|
2151
|
+
}
|
|
2152
|
+
}, async (span) => {
|
|
2153
|
+
try {
|
|
2154
|
+
const res = await next();
|
|
2155
|
+
span.setAttributes({
|
|
2156
|
+
"http.status_code": ctx.res.status
|
|
2157
|
+
});
|
|
2158
|
+
if (ctx.res.status >= 500) {
|
|
2159
|
+
span.setStatus({ code: this.api.SpanStatusCode.ERROR });
|
|
2160
|
+
} else {
|
|
2161
|
+
span.setStatus({ code: this.api.SpanStatusCode.OK });
|
|
2162
|
+
}
|
|
2163
|
+
return res;
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
span.recordException(err);
|
|
2166
|
+
span.setStatus({ code: this.api.SpanStatusCode.ERROR, message: err.message });
|
|
2167
|
+
throw err;
|
|
2168
|
+
} finally {
|
|
2169
|
+
span.end();
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
function traceMiddleware(fn, name) {
|
|
2176
|
+
let api;
|
|
2177
|
+
try {
|
|
2178
|
+
api = require("@opentelemetry/api");
|
|
2179
|
+
} catch {
|
|
2180
|
+
}
|
|
2181
|
+
if (!api) return fn;
|
|
2182
|
+
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
2183
|
+
const middlewareName = name || fn.name || "anonymous middleware";
|
|
2184
|
+
return async (ctx, next) => {
|
|
2185
|
+
return tracer.startActiveSpan(`middleware - ${middlewareName}`, {
|
|
2186
|
+
kind: api.SpanKind.INTERNAL,
|
|
2187
|
+
attributes: {
|
|
2188
|
+
"code.function": middlewareName,
|
|
2189
|
+
"component": "shokupan.middleware"
|
|
2190
|
+
}
|
|
2191
|
+
}, async (span) => {
|
|
2192
|
+
try {
|
|
2193
|
+
const result = await fn(ctx, next);
|
|
2194
|
+
return result;
|
|
2195
|
+
} catch (err) {
|
|
2196
|
+
span.recordException(err);
|
|
2197
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
2198
|
+
throw err;
|
|
2199
|
+
} finally {
|
|
2200
|
+
span.end();
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function traceHandler(fn, name) {
|
|
2206
|
+
let api;
|
|
2207
|
+
try {
|
|
2208
|
+
api = require("@opentelemetry/api");
|
|
2209
|
+
} catch {
|
|
2210
|
+
}
|
|
2211
|
+
if (!api) return fn;
|
|
2212
|
+
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
2213
|
+
return async function(...args) {
|
|
2214
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
2215
|
+
kind: api.SpanKind.INTERNAL,
|
|
2216
|
+
attributes: {
|
|
2217
|
+
"http.route": name,
|
|
2218
|
+
"component": "shokupan.route"
|
|
2219
|
+
}
|
|
2220
|
+
}, async (span) => {
|
|
2221
|
+
try {
|
|
2222
|
+
const result = await fn.apply(this, args);
|
|
2223
|
+
return result;
|
|
2224
|
+
} catch (err) {
|
|
2225
|
+
span.recordException(err);
|
|
2226
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
2227
|
+
throw err;
|
|
2228
|
+
} finally {
|
|
2229
|
+
span.end();
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
class ResilienceFactory {
|
|
2235
|
+
static createPolicy(config) {
|
|
2236
|
+
const policies = [];
|
|
2237
|
+
if (config.retry) {
|
|
2238
|
+
const builder = cockatiel.handleAll;
|
|
2239
|
+
let retries = (config.retry.attempts ?? 3) - 1;
|
|
2240
|
+
if (retries < 0) retries = 0;
|
|
2241
|
+
let retryPolicy;
|
|
2242
|
+
if (config.retry.backoff === "exponential") {
|
|
2243
|
+
retryPolicy = cockatiel.retry(builder, {
|
|
2244
|
+
maxAttempts: retries,
|
|
2245
|
+
backoff: new cockatiel.ExponentialBackoff({
|
|
2246
|
+
initialDelay: config.retry.delay || 1e3,
|
|
2247
|
+
maxDelay: config.retry.maxDelay || 3e4
|
|
2248
|
+
})
|
|
2249
|
+
});
|
|
2250
|
+
} else {
|
|
2251
|
+
retryPolicy = cockatiel.retry(builder, {
|
|
2252
|
+
maxAttempts: retries,
|
|
2253
|
+
backoff: new cockatiel.ConstantBackoff(config.retry.delay || 1e3)
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
policies.push(retryPolicy);
|
|
2257
|
+
}
|
|
2258
|
+
if (config.circuitBreaker) {
|
|
2259
|
+
const builder = cockatiel.handleAll;
|
|
2260
|
+
const breaker = cockatiel.circuitBreaker(builder, {
|
|
2261
|
+
halfOpenAfter: config.circuitBreaker.resetTimeout || 1e4,
|
|
2262
|
+
breaker: new cockatiel.ConsecutiveBreaker(config.circuitBreaker.threshold || 5)
|
|
2263
|
+
});
|
|
2264
|
+
policies.push(breaker);
|
|
2265
|
+
}
|
|
2266
|
+
if (config.timeout) {
|
|
2267
|
+
policies.push(cockatiel.timeout(config.timeout, { strategy: cockatiel.TimeoutStrategy.Aggressive, abortOnReturn: true }));
|
|
2268
|
+
}
|
|
2269
|
+
if (config.bulkhead) {
|
|
2270
|
+
policies.push(cockatiel.bulkhead(config.bulkhead));
|
|
2271
|
+
}
|
|
2272
|
+
if (config.fallback !== void 0) {
|
|
2273
|
+
const builder = cockatiel.handleAll;
|
|
2274
|
+
const fb = cockatiel.fallback(builder, config.fallback);
|
|
2275
|
+
policies.push(fb);
|
|
2276
|
+
}
|
|
2277
|
+
if (policies.length === 0) {
|
|
2278
|
+
return { execute: (fn) => fn() };
|
|
2279
|
+
}
|
|
2280
|
+
return cockatiel.wrap(...policies.reverse());
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2010
2283
|
const metadataStore = /* @__PURE__ */ new WeakMap();
|
|
2011
2284
|
function defineMetadata(key, value, target, propertyKey) {
|
|
2012
2285
|
let targetMetadata = metadataStore.get(target);
|
|
@@ -2103,29 +2376,6 @@ const di = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
|
2103
2376
|
__proto__: null,
|
|
2104
2377
|
Container
|
|
2105
2378
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
2106
|
-
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
2107
|
-
function traceHandler(fn, name) {
|
|
2108
|
-
return async function(...args) {
|
|
2109
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
2110
|
-
kind: api.SpanKind.INTERNAL,
|
|
2111
|
-
attributes: {
|
|
2112
|
-
"http.route": name,
|
|
2113
|
-
"component": "shokupan.route"
|
|
2114
|
-
}
|
|
2115
|
-
}, async (span) => {
|
|
2116
|
-
try {
|
|
2117
|
-
const result = await fn.apply(this, args);
|
|
2118
|
-
return result;
|
|
2119
|
-
} catch (err) {
|
|
2120
|
-
span.recordException(err);
|
|
2121
|
-
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
2122
|
-
throw err;
|
|
2123
|
-
} finally {
|
|
2124
|
-
span.end();
|
|
2125
|
-
}
|
|
2126
|
-
});
|
|
2127
|
-
};
|
|
2128
|
-
}
|
|
2129
2379
|
function getCallerInfo(skipFrames = 1) {
|
|
2130
2380
|
let file = "unknown";
|
|
2131
2381
|
let line = 0;
|
|
@@ -2143,6 +2393,7 @@ function getCallerInfo(skipFrames = 1) {
|
|
|
2143
2393
|
if (l.includes("src/router.ts")) continue;
|
|
2144
2394
|
if (l.includes("src/util/decorators.ts")) continue;
|
|
2145
2395
|
if (l.includes("src/shokupan.ts")) continue;
|
|
2396
|
+
if (l.includes("src/plugins/application/openapi/openapi.ts")) continue;
|
|
2146
2397
|
found++;
|
|
2147
2398
|
if (found >= skipFrames) {
|
|
2148
2399
|
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
@@ -2211,6 +2462,10 @@ class ControllerScanner {
|
|
|
2211
2462
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
2212
2463
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
2213
2464
|
const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
|
|
2465
|
+
const mcpTools = instance[$mcpTools] || proto && proto[$mcpTools];
|
|
2466
|
+
const mcpPrompts = instance[$mcpPrompts] || proto && proto[$mcpPrompts];
|
|
2467
|
+
const mcpResources = instance[$mcpResources] || proto && proto[$mcpResources];
|
|
2468
|
+
const resilienceConfigMap = instance[$resilienceConfig] || proto && proto[$resilienceConfig];
|
|
2214
2469
|
let routesAttached = 0;
|
|
2215
2470
|
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
2216
2471
|
const name = Array.from(methods)[i];
|
|
@@ -2335,6 +2590,14 @@ class ControllerScanner {
|
|
|
2335
2590
|
return composed(ctx, () => wrappedHandler(ctx));
|
|
2336
2591
|
};
|
|
2337
2592
|
}
|
|
2593
|
+
const config = resilienceConfigMap?.get(name);
|
|
2594
|
+
if (config) {
|
|
2595
|
+
const policy = ResilienceFactory.createPolicy(config);
|
|
2596
|
+
const baseHandler = finalHandler;
|
|
2597
|
+
finalHandler = async (ctx) => {
|
|
2598
|
+
return policy.execute(() => baseHandler(ctx));
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2338
2601
|
finalHandler.originalHandler = originalHandler;
|
|
2339
2602
|
if (finalHandler !== wrappedHandler) {
|
|
2340
2603
|
wrappedHandler.originalHandler = originalHandler;
|
|
@@ -2391,6 +2654,25 @@ class ControllerScanner {
|
|
|
2391
2654
|
wrappedHandler.originalHandler = originalHandler;
|
|
2392
2655
|
router.event(eventConfig.eventName, wrappedHandler);
|
|
2393
2656
|
}
|
|
2657
|
+
const toolConfig = mcpTools?.get(name);
|
|
2658
|
+
if (toolConfig) {
|
|
2659
|
+
const handler = originalHandler.bind(instance);
|
|
2660
|
+
router.tool(toolConfig.name || name, toolConfig.inputSchema, handler);
|
|
2661
|
+
}
|
|
2662
|
+
const promptConfig = mcpPrompts?.get(name);
|
|
2663
|
+
if (promptConfig) {
|
|
2664
|
+
const handler = originalHandler.bind(instance);
|
|
2665
|
+
router.prompt(promptConfig.name || name, promptConfig.arguments, handler);
|
|
2666
|
+
}
|
|
2667
|
+
const resourceConfig = mcpResources?.get(name);
|
|
2668
|
+
if (resourceConfig) {
|
|
2669
|
+
const handler = originalHandler.bind(instance);
|
|
2670
|
+
router.resource(resourceConfig.uri, {
|
|
2671
|
+
name: resourceConfig.name || name,
|
|
2672
|
+
description: resourceConfig.description,
|
|
2673
|
+
mimeType: resourceConfig.mimeType
|
|
2674
|
+
}, handler);
|
|
2675
|
+
}
|
|
2394
2676
|
}
|
|
2395
2677
|
if (routesAttached === 0) {
|
|
2396
2678
|
console.warn(`No routes attached to controller ${instance.constructor.name}`);
|
|
@@ -2421,71 +2703,177 @@ function getErrorStatus(err) {
|
|
|
2421
2703
|
}
|
|
2422
2704
|
return 500;
|
|
2423
2705
|
}
|
|
2706
|
+
class NotFoundError extends HttpError {
|
|
2707
|
+
constructor(message = "Not Found") {
|
|
2708
|
+
super(message, 404);
|
|
2709
|
+
this.name = "NotFoundError";
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2424
2712
|
class EventError extends HttpError {
|
|
2425
2713
|
constructor(message = "Event Error") {
|
|
2426
2714
|
super(message, 500);
|
|
2427
2715
|
this.name = "EventError";
|
|
2428
2716
|
}
|
|
2429
2717
|
}
|
|
2718
|
+
class McpProtocol {
|
|
2719
|
+
tools = /* @__PURE__ */ new Map();
|
|
2720
|
+
prompts = /* @__PURE__ */ new Map();
|
|
2721
|
+
resources = /* @__PURE__ */ new Map();
|
|
2722
|
+
constructor(tools = [], prompts = [], resources = []) {
|
|
2723
|
+
tools.forEach((t) => this.tools.set(t.name, t));
|
|
2724
|
+
prompts.forEach((p) => this.prompts.set(p.name, p));
|
|
2725
|
+
resources.forEach((r) => this.resources.set(r.uri, r));
|
|
2726
|
+
}
|
|
2727
|
+
addTool(tool) {
|
|
2728
|
+
this.tools.set(tool.name, tool);
|
|
2729
|
+
}
|
|
2730
|
+
addPrompt(prompt) {
|
|
2731
|
+
this.prompts.set(prompt.name, prompt);
|
|
2732
|
+
}
|
|
2733
|
+
addResource(resource) {
|
|
2734
|
+
this.resources.set(resource.uri, resource);
|
|
2735
|
+
}
|
|
2736
|
+
merge(other) {
|
|
2737
|
+
other.tools.forEach((t) => this.tools.set(t.name, t));
|
|
2738
|
+
other.prompts.forEach((p) => this.prompts.set(p.name, p));
|
|
2739
|
+
other.resources.forEach((r) => this.resources.set(r.uri, r));
|
|
2740
|
+
}
|
|
2741
|
+
async handleMessage(message) {
|
|
2742
|
+
if (message.jsonrpc !== "2.0") {
|
|
2743
|
+
return this.error(message.id, -32600, "Invalid Request");
|
|
2744
|
+
}
|
|
2745
|
+
try {
|
|
2746
|
+
switch (message.method) {
|
|
2747
|
+
case "initialize":
|
|
2748
|
+
return this.success(message.id, {
|
|
2749
|
+
protocolVersion: "2024-11-05",
|
|
2750
|
+
serverInfo: {
|
|
2751
|
+
name: "Shokupan MCP",
|
|
2752
|
+
version: "1.0.0"
|
|
2753
|
+
},
|
|
2754
|
+
capabilities: {
|
|
2755
|
+
tools: this.tools.size > 0 ? {} : void 0,
|
|
2756
|
+
prompts: this.prompts.size > 0 ? {} : void 0,
|
|
2757
|
+
resources: this.resources.size > 0 ? {} : void 0
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
case "ping":
|
|
2761
|
+
return this.success(message.id, {});
|
|
2762
|
+
case "tools/list":
|
|
2763
|
+
if (this.tools.size === 0) return this.success(message.id, { tools: [] });
|
|
2764
|
+
return this.success(message.id, {
|
|
2765
|
+
tools: Array.from(this.tools.values()).map((t) => ({
|
|
2766
|
+
name: t.name,
|
|
2767
|
+
description: t.description,
|
|
2768
|
+
inputSchema: t.inputSchema || { type: "object", properties: {} }
|
|
2769
|
+
}))
|
|
2770
|
+
});
|
|
2771
|
+
case "tools/call": {
|
|
2772
|
+
if (!message.params || !message.params.name) {
|
|
2773
|
+
return this.error(message.id, -32602, "Invalid params: name required");
|
|
2774
|
+
}
|
|
2775
|
+
const tool = this.tools.get(message.params.name);
|
|
2776
|
+
if (!tool) {
|
|
2777
|
+
return this.error(message.id, -32601, `Tool not found: ${message.params.name}`);
|
|
2778
|
+
}
|
|
2779
|
+
try {
|
|
2780
|
+
const result = await tool.handler(message.params.arguments || {});
|
|
2781
|
+
return this.success(message.id, result);
|
|
2782
|
+
} catch (e) {
|
|
2783
|
+
return {
|
|
2784
|
+
jsonrpc: "2.0",
|
|
2785
|
+
id: message.id ?? null,
|
|
2786
|
+
result: {
|
|
2787
|
+
isError: true,
|
|
2788
|
+
content: [{ type: "text", text: e.message || String(e) }]
|
|
2789
|
+
}
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
case "prompts/list":
|
|
2794
|
+
if (this.prompts.size === 0) return this.success(message.id, { prompts: [] });
|
|
2795
|
+
return this.success(message.id, {
|
|
2796
|
+
prompts: Array.from(this.prompts.values()).map((p) => ({
|
|
2797
|
+
name: p.name,
|
|
2798
|
+
description: p.description,
|
|
2799
|
+
arguments: p.arguments
|
|
2800
|
+
}))
|
|
2801
|
+
});
|
|
2802
|
+
case "prompts/get": {
|
|
2803
|
+
if (!message.params || !message.params.name) {
|
|
2804
|
+
return this.error(message.id, -32602, "Invalid params: name required");
|
|
2805
|
+
}
|
|
2806
|
+
const prompt = this.prompts.get(message.params.name);
|
|
2807
|
+
if (!prompt) {
|
|
2808
|
+
return this.error(message.id, -32601, `Prompt not found: ${message.params.name}`);
|
|
2809
|
+
}
|
|
2810
|
+
const result = await prompt.handler(message.params.arguments || {});
|
|
2811
|
+
return this.success(message.id, result);
|
|
2812
|
+
}
|
|
2813
|
+
case "resources/list":
|
|
2814
|
+
if (this.resources.size === 0) return this.success(message.id, { resources: [] });
|
|
2815
|
+
return this.success(message.id, {
|
|
2816
|
+
resources: Array.from(this.resources.values()).map((r) => ({
|
|
2817
|
+
uri: r.uri,
|
|
2818
|
+
name: r.name,
|
|
2819
|
+
description: r.description,
|
|
2820
|
+
mimeType: r.mimeType
|
|
2821
|
+
}))
|
|
2822
|
+
});
|
|
2823
|
+
case "resources/read": {
|
|
2824
|
+
if (!message.params || !message.params.uri) {
|
|
2825
|
+
return this.error(message.id, -32602, "Invalid params: uri required");
|
|
2826
|
+
}
|
|
2827
|
+
let resource = this.resources.get(message.params.uri);
|
|
2828
|
+
if (!resource) {
|
|
2829
|
+
return this.error(message.id, -32601, `Resource not found: ${message.params.uri}`);
|
|
2830
|
+
}
|
|
2831
|
+
const result = await resource.handler(message.params.uri);
|
|
2832
|
+
return this.success(message.id, result);
|
|
2833
|
+
}
|
|
2834
|
+
default:
|
|
2835
|
+
if (message.id === void 0) return null;
|
|
2836
|
+
return this.error(message.id, -32601, "Method not found");
|
|
2837
|
+
}
|
|
2838
|
+
} catch (err) {
|
|
2839
|
+
return this.error(message.id, -32603, "Internal Error", err.message);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
success(id, result) {
|
|
2843
|
+
return {
|
|
2844
|
+
jsonrpc: "2.0",
|
|
2845
|
+
id: id ?? null,
|
|
2846
|
+
result
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
error(id, code, message, data) {
|
|
2850
|
+
return {
|
|
2851
|
+
jsonrpc: "2.0",
|
|
2852
|
+
id: id ?? null,
|
|
2853
|
+
error: { code, message, data }
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2430
2857
|
class MiddlewareTracker {
|
|
2431
2858
|
static wrap(handler, context) {
|
|
2432
2859
|
const { file, line, name, isBuiltin, pluginName } = context;
|
|
2433
2860
|
const handlerName = name || handler.name || "anonymous";
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
try {
|
|
2441
|
-
ctx.handlerStack.push({
|
|
2442
|
-
name: handlerName,
|
|
2443
|
-
file,
|
|
2444
|
-
line,
|
|
2445
|
-
isBuiltin,
|
|
2446
|
-
startTime,
|
|
2447
|
-
duration: -1
|
|
2448
|
-
});
|
|
2449
|
-
return await handler(ctx, next);
|
|
2450
|
-
} catch (e) {
|
|
2451
|
-
error = e;
|
|
2452
|
-
throw e;
|
|
2453
|
-
} finally {
|
|
2454
|
-
const duration = performance.now() - startTime;
|
|
2455
|
-
const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
|
|
2456
|
-
if (stackItem && stackItem.name === handlerName) {
|
|
2457
|
-
stackItem.duration = duration;
|
|
2861
|
+
try {
|
|
2862
|
+
handler.metadata = context;
|
|
2863
|
+
if (!handler.name || handler.name === "anonymous") {
|
|
2864
|
+
try {
|
|
2865
|
+
Object.defineProperty(handler, "name", { value: handlerName, configurable: true });
|
|
2866
|
+
} catch (e) {
|
|
2458
2867
|
}
|
|
2459
|
-
Promise.resolve().then(async () => {
|
|
2460
|
-
try {
|
|
2461
|
-
const db = ctx.app?.db;
|
|
2462
|
-
if (!db) return;
|
|
2463
|
-
const timestamp = Date.now();
|
|
2464
|
-
await db.upsert(new surrealdb.RecordId("middleware_tracking", {
|
|
2465
|
-
timestamp,
|
|
2466
|
-
name: handlerName
|
|
2467
|
-
}), {
|
|
2468
|
-
name: handlerName,
|
|
2469
|
-
path: ctx.path,
|
|
2470
|
-
timestamp,
|
|
2471
|
-
duration,
|
|
2472
|
-
file,
|
|
2473
|
-
line,
|
|
2474
|
-
error: error ? String(error) : void 0,
|
|
2475
|
-
metadata: {
|
|
2476
|
-
isBuiltin,
|
|
2477
|
-
pluginName
|
|
2478
|
-
}
|
|
2479
|
-
});
|
|
2480
|
-
} catch (err) {
|
|
2481
|
-
}
|
|
2482
|
-
});
|
|
2483
2868
|
}
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2869
|
+
} catch (e) {
|
|
2870
|
+
const wrapped = handler.bind(null);
|
|
2871
|
+
wrapped.metadata = context;
|
|
2872
|
+
Object.defineProperty(wrapped, "name", { value: handlerName });
|
|
2873
|
+
wrapped.originalHandler = handler.originalHandler || handler;
|
|
2874
|
+
return wrapped;
|
|
2875
|
+
}
|
|
2876
|
+
return handler;
|
|
2489
2877
|
}
|
|
2490
2878
|
}
|
|
2491
2879
|
class ShokupanRequestBase {
|
|
@@ -2699,6 +3087,7 @@ class ShokupanRouter {
|
|
|
2699
3087
|
trie = new RouterTrie();
|
|
2700
3088
|
metadata;
|
|
2701
3089
|
// Metadata for the router itself
|
|
3090
|
+
mcpProtocol = new McpProtocol();
|
|
2702
3091
|
currentGuards = [];
|
|
2703
3092
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
2704
3093
|
/**
|
|
@@ -2824,6 +3213,39 @@ class ShokupanRouter {
|
|
|
2824
3213
|
handlers.push(handler);
|
|
2825
3214
|
return this;
|
|
2826
3215
|
}
|
|
3216
|
+
/**
|
|
3217
|
+
* Registers an MCP Tool.
|
|
3218
|
+
*/
|
|
3219
|
+
tool(name, schema, handler) {
|
|
3220
|
+
this.mcpProtocol.addTool({
|
|
3221
|
+
name,
|
|
3222
|
+
inputSchema: schema,
|
|
3223
|
+
handler
|
|
3224
|
+
});
|
|
3225
|
+
return this;
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* Registers an MCP Prompt.
|
|
3229
|
+
*/
|
|
3230
|
+
prompt(name, args, handler) {
|
|
3231
|
+
this.mcpProtocol.addPrompt({
|
|
3232
|
+
name,
|
|
3233
|
+
arguments: args,
|
|
3234
|
+
handler
|
|
3235
|
+
});
|
|
3236
|
+
return this;
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* Registers an MCP Resource.
|
|
3240
|
+
*/
|
|
3241
|
+
resource(uri, options, handler) {
|
|
3242
|
+
this.mcpProtocol.addResource({
|
|
3243
|
+
uri,
|
|
3244
|
+
handler,
|
|
3245
|
+
...options
|
|
3246
|
+
});
|
|
3247
|
+
return this;
|
|
3248
|
+
}
|
|
2827
3249
|
/**
|
|
2828
3250
|
* Finds an event handler(s) by name.
|
|
2829
3251
|
*/
|
|
@@ -3122,59 +3544,48 @@ class ShokupanRouter {
|
|
|
3122
3544
|
}
|
|
3123
3545
|
}
|
|
3124
3546
|
}
|
|
3125
|
-
let wrappedHandler = async (ctx) => {
|
|
3126
|
-
return handler(ctx);
|
|
3127
|
-
};
|
|
3128
|
-
wrappedHandler.originalHandler = handler.originalHandler || handler;
|
|
3129
|
-
const routeGuards = [...this.currentGuards];
|
|
3130
3547
|
const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
|
|
3131
|
-
|
|
3132
|
-
|
|
3548
|
+
const effectiveRenderer = renderer ?? this.config?.renderer ?? this.rootConfig?.renderer;
|
|
3549
|
+
const routeGuards = [...this.currentGuards];
|
|
3550
|
+
let wrappedHandler = handler;
|
|
3551
|
+
if (effectiveTimeout && effectiveTimeout > 0 || effectiveRenderer || routeGuards.length > 0) {
|
|
3552
|
+
const originalHandler = handler;
|
|
3133
3553
|
wrappedHandler = async (ctx) => {
|
|
3134
|
-
if (ctx.server) {
|
|
3554
|
+
if (effectiveTimeout && effectiveTimeout > 0 && ctx.server) {
|
|
3135
3555
|
ctx.server.timeout(ctx.req, effectiveTimeout / 1e3);
|
|
3136
3556
|
}
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
}
|
|
3557
|
+
if (effectiveRenderer) {
|
|
3558
|
+
ctx.setRenderer(effectiveRenderer);
|
|
3559
|
+
}
|
|
3560
|
+
if (routeGuards.length > 0) {
|
|
3561
|
+
for (let i = 0; i < routeGuards.length; i++) {
|
|
3562
|
+
const guard = routeGuards[i];
|
|
3563
|
+
let guardPassed = false;
|
|
3564
|
+
let nextCalled = false;
|
|
3565
|
+
const next = () => {
|
|
3566
|
+
nextCalled = true;
|
|
3567
|
+
return Promise.resolve();
|
|
3568
|
+
};
|
|
3569
|
+
try {
|
|
3570
|
+
const result = await guard.handler(ctx, next);
|
|
3571
|
+
if (result === true || nextCalled) {
|
|
3572
|
+
guardPassed = true;
|
|
3573
|
+
} else if (result !== void 0 && result !== null && result !== false) {
|
|
3574
|
+
return result;
|
|
3575
|
+
} else {
|
|
3576
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
3577
|
+
}
|
|
3578
|
+
} catch (error) {
|
|
3579
|
+
throw error;
|
|
3580
|
+
}
|
|
3581
|
+
if (!guardPassed) {
|
|
3159
3582
|
return ctx.json({ error: "Forbidden" }, 403);
|
|
3160
3583
|
}
|
|
3161
|
-
} catch (error) {
|
|
3162
|
-
throw error;
|
|
3163
|
-
}
|
|
3164
|
-
if (!guardPassed) {
|
|
3165
|
-
return ctx.json({ error: "Forbidden" }, 403);
|
|
3166
3584
|
}
|
|
3167
3585
|
}
|
|
3168
|
-
return
|
|
3169
|
-
};
|
|
3170
|
-
}
|
|
3171
|
-
const effectiveRenderer = renderer ?? this.config?.renderer ?? this.rootConfig?.renderer;
|
|
3172
|
-
if (effectiveRenderer) {
|
|
3173
|
-
const innerHandler = wrappedHandler;
|
|
3174
|
-
wrappedHandler = async (ctx) => {
|
|
3175
|
-
ctx.setRenderer(effectiveRenderer);
|
|
3176
|
-
return innerHandler(ctx);
|
|
3586
|
+
return originalHandler(ctx);
|
|
3177
3587
|
};
|
|
3588
|
+
wrappedHandler.originalHandler = handler.originalHandler || handler;
|
|
3178
3589
|
}
|
|
3179
3590
|
const { file, line } = metadata || getCallerInfo();
|
|
3180
3591
|
wrappedHandler = MiddlewareTracker.wrap(wrappedHandler, {
|
|
@@ -3875,6 +4286,32 @@ class SurrealDatastore {
|
|
|
3875
4286
|
return this.db.close();
|
|
3876
4287
|
}
|
|
3877
4288
|
}
|
|
4289
|
+
const kContext = /* @__PURE__ */ Symbol("kContext");
|
|
4290
|
+
let patched = false;
|
|
4291
|
+
function enablePromisePatch() {
|
|
4292
|
+
if (patched) return;
|
|
4293
|
+
patched = true;
|
|
4294
|
+
const OriginalPromise = global.Promise;
|
|
4295
|
+
global.Promise = class PatchedPromise extends OriginalPromise {
|
|
4296
|
+
[kContext];
|
|
4297
|
+
constructor(executor) {
|
|
4298
|
+
const store = asyncContext.getStore();
|
|
4299
|
+
const stack = new Error().stack || "No parent stack";
|
|
4300
|
+
super(executor);
|
|
4301
|
+
this[kContext] = {
|
|
4302
|
+
store,
|
|
4303
|
+
stack
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
4306
|
+
};
|
|
4307
|
+
for (const prop of Object.getOwnPropertyNames(OriginalPromise)) {
|
|
4308
|
+
if (prop !== "prototype" && prop !== "length" && prop !== "name") {
|
|
4309
|
+
if (typeof OriginalPromise[prop] === "function") {
|
|
4310
|
+
global.Promise[prop] = OriginalPromise[prop];
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
3878
4315
|
const defaults = {
|
|
3879
4316
|
port: 3e3,
|
|
3880
4317
|
hostname: "localhost",
|
|
@@ -3918,15 +4355,32 @@ class Shokupan extends ShokupanRouter {
|
|
|
3918
4355
|
line,
|
|
3919
4356
|
name: "ShokupanApplication"
|
|
3920
4357
|
};
|
|
3921
|
-
if (this.applicationConfig.
|
|
4358
|
+
if (this.applicationConfig.defaultSecurityHeaders) {
|
|
3922
4359
|
const { SecurityHeaders: SecurityHeaders2 } = require("./plugins/middleware/security-headers");
|
|
3923
|
-
this.use(SecurityHeaders2(this.applicationConfig.
|
|
4360
|
+
this.use(SecurityHeaders2(this.applicationConfig.defaultSecurityHeaders === true ? {} : this.applicationConfig.defaultSecurityHeaders));
|
|
3924
4361
|
}
|
|
3925
4362
|
if (this.applicationConfig.adapter !== "wintercg") {
|
|
3926
4363
|
this.dbPromise = this.initDatastore().catch((err) => {
|
|
3927
4364
|
this.logger?.debug("Failed to initialize default datastore", { error: err });
|
|
3928
4365
|
});
|
|
3929
4366
|
}
|
|
4367
|
+
if (this.applicationConfig.enablePromiseMonkeypatch) {
|
|
4368
|
+
enablePromisePatch();
|
|
4369
|
+
const processRef = typeof process !== "undefined" ? process : void 0;
|
|
4370
|
+
if (processRef && processRef.on) {
|
|
4371
|
+
processRef.on("unhandledRejection", (reason, promise) => {
|
|
4372
|
+
const ctx = promise?.[kContext];
|
|
4373
|
+
if (ctx && ctx.store && ctx.store.app === this) {
|
|
4374
|
+
const { requestId } = ctx.store;
|
|
4375
|
+
this.logger.error("Unhandled Rejection in Shokupan Request", {
|
|
4376
|
+
error: reason,
|
|
4377
|
+
requestId,
|
|
4378
|
+
creationStack: ctx.stack
|
|
4379
|
+
});
|
|
4380
|
+
}
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
3930
4384
|
}
|
|
3931
4385
|
async initDatastore() {
|
|
3932
4386
|
let engines = this.applicationConfig.surreal?.engines;
|
|
@@ -4012,7 +4466,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
4012
4466
|
this.get("/.well-known/openapi.yaml", async (ctx) => {
|
|
4013
4467
|
try {
|
|
4014
4468
|
await this.openApiSpecPromise;
|
|
4015
|
-
const
|
|
4469
|
+
const { dump } = await import("js-yaml");
|
|
4470
|
+
const yaml = dump(this.openApiSpec);
|
|
4016
4471
|
return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
|
|
4017
4472
|
} catch (e) {
|
|
4018
4473
|
this.logger?.error("Failed to generate OpenAPI YAML", { error: e });
|
|
@@ -4203,7 +4658,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
4203
4658
|
*/
|
|
4204
4659
|
async fetch(req, server) {
|
|
4205
4660
|
if (this.applicationConfig.enableTracing) {
|
|
4206
|
-
const
|
|
4661
|
+
const { trace, context } = await import("@opentelemetry/api");
|
|
4662
|
+
const tracer = trace.getTracer("shokupan.application");
|
|
4207
4663
|
const store = asyncContext.getStore();
|
|
4208
4664
|
const attrs = {
|
|
4209
4665
|
attributes: {
|
|
@@ -4212,8 +4668,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
4212
4668
|
}
|
|
4213
4669
|
};
|
|
4214
4670
|
const parent = store?.span;
|
|
4215
|
-
const ctx = parent ?
|
|
4216
|
-
return
|
|
4671
|
+
const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
|
|
4672
|
+
return tracer.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
|
|
4217
4673
|
const ctxStore = new RequestContextStore();
|
|
4218
4674
|
ctxStore.span = span;
|
|
4219
4675
|
ctxStore.request = req;
|
|
@@ -4221,16 +4677,19 @@ class Shokupan extends ShokupanRouter {
|
|
|
4221
4677
|
});
|
|
4222
4678
|
}
|
|
4223
4679
|
if (this.applicationConfig.enableAsyncLocalStorage) {
|
|
4680
|
+
const requestId = this.applicationConfig.idGenerator?.() ?? nanoid.nanoid();
|
|
4224
4681
|
const ctxStore = new RequestContextStore();
|
|
4225
4682
|
ctxStore.request = req;
|
|
4226
|
-
|
|
4683
|
+
ctxStore["requestId"] = requestId;
|
|
4684
|
+
ctxStore["app"] = this;
|
|
4685
|
+
return asyncContext.run(ctxStore, () => this.handleRequest(req, server, requestId));
|
|
4227
4686
|
}
|
|
4228
4687
|
return this.handleRequest(req, server);
|
|
4229
4688
|
}
|
|
4230
|
-
async handleRequest(req, server) {
|
|
4689
|
+
async handleRequest(req, server, requestId) {
|
|
4231
4690
|
const request = req;
|
|
4232
4691
|
const controller = new AbortController();
|
|
4233
|
-
const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
|
|
4692
|
+
const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking, requestId);
|
|
4234
4693
|
const handle = async () => {
|
|
4235
4694
|
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
4236
4695
|
const msg = "Too Many Requests (CPU Backpressure)";
|
|
@@ -4251,9 +4710,91 @@ class Shokupan extends ShokupanRouter {
|
|
|
4251
4710
|
ctx[$routeMatched] = true;
|
|
4252
4711
|
ctx.params = match.params;
|
|
4253
4712
|
if (bodyParsing) await bodyParsing;
|
|
4713
|
+
if (this.applicationConfig.enableMiddlewareTracking) {
|
|
4714
|
+
const handler = match.handler;
|
|
4715
|
+
const meta = handler.metadata;
|
|
4716
|
+
if (meta) {
|
|
4717
|
+
const trackingStartTime = performance.now();
|
|
4718
|
+
const handlerName = meta.name || handler.name || "anonymous";
|
|
4719
|
+
ctx.handlerStack.push({
|
|
4720
|
+
name: handlerName,
|
|
4721
|
+
file: meta.file,
|
|
4722
|
+
line: meta.line,
|
|
4723
|
+
isBuiltin: meta.isBuiltin,
|
|
4724
|
+
startTime: trackingStartTime,
|
|
4725
|
+
duration: -1
|
|
4726
|
+
});
|
|
4727
|
+
try {
|
|
4728
|
+
const res = await handler(ctx);
|
|
4729
|
+
const duration = performance.now() - trackingStartTime;
|
|
4730
|
+
const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
|
|
4731
|
+
if (stackItem) stackItem.duration = duration;
|
|
4732
|
+
Promise.resolve().then(async () => {
|
|
4733
|
+
try {
|
|
4734
|
+
const db = this.db;
|
|
4735
|
+
if (!db) return;
|
|
4736
|
+
const timestamp = Date.now();
|
|
4737
|
+
await db.upsert(new surrealdb.RecordId("middleware_tracking", {
|
|
4738
|
+
timestamp,
|
|
4739
|
+
name: handlerName
|
|
4740
|
+
}), {
|
|
4741
|
+
name: handlerName,
|
|
4742
|
+
path: ctx.path,
|
|
4743
|
+
timestamp,
|
|
4744
|
+
duration,
|
|
4745
|
+
file: meta.file,
|
|
4746
|
+
line: meta.line,
|
|
4747
|
+
error: void 0,
|
|
4748
|
+
metadata: {
|
|
4749
|
+
isBuiltin: meta.isBuiltin,
|
|
4750
|
+
pluginName: meta.pluginName
|
|
4751
|
+
}
|
|
4752
|
+
});
|
|
4753
|
+
} catch (e) {
|
|
4754
|
+
}
|
|
4755
|
+
});
|
|
4756
|
+
return res;
|
|
4757
|
+
} catch (err) {
|
|
4758
|
+
const duration = performance.now() - trackingStartTime;
|
|
4759
|
+
const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
|
|
4760
|
+
if (stackItem) stackItem.duration = duration;
|
|
4761
|
+
Promise.resolve().then(async () => {
|
|
4762
|
+
try {
|
|
4763
|
+
const db = this.db;
|
|
4764
|
+
if (!db) return;
|
|
4765
|
+
const timestamp = Date.now();
|
|
4766
|
+
await db.upsert(new surrealdb.RecordId("middleware_tracking", {
|
|
4767
|
+
timestamp,
|
|
4768
|
+
name: handlerName
|
|
4769
|
+
}), {
|
|
4770
|
+
name: handlerName,
|
|
4771
|
+
path: ctx.path,
|
|
4772
|
+
timestamp,
|
|
4773
|
+
duration,
|
|
4774
|
+
file: meta.file,
|
|
4775
|
+
line: meta.line,
|
|
4776
|
+
error: String(err),
|
|
4777
|
+
metadata: {
|
|
4778
|
+
isBuiltin: meta.isBuiltin,
|
|
4779
|
+
pluginName: meta.pluginName
|
|
4780
|
+
}
|
|
4781
|
+
});
|
|
4782
|
+
} catch (e) {
|
|
4783
|
+
}
|
|
4784
|
+
});
|
|
4785
|
+
throw err;
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4254
4789
|
return match.handler(ctx);
|
|
4255
4790
|
}
|
|
4256
|
-
|
|
4791
|
+
if (ctx.upgrade()) {
|
|
4792
|
+
return void 0;
|
|
4793
|
+
}
|
|
4794
|
+
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
4795
|
+
return ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
4796
|
+
}
|
|
4797
|
+
throw new NotFoundError();
|
|
4257
4798
|
});
|
|
4258
4799
|
let response;
|
|
4259
4800
|
if (result instanceof Response) {
|
|
@@ -4272,14 +4813,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
4272
4813
|
}
|
|
4273
4814
|
response = ctx.send(null, { status, headers: ctx.response.headers });
|
|
4274
4815
|
} else {
|
|
4275
|
-
|
|
4276
|
-
return void 0;
|
|
4277
|
-
}
|
|
4278
|
-
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
4279
|
-
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
4280
|
-
} else {
|
|
4281
|
-
response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
|
|
4282
|
-
}
|
|
4816
|
+
throw new NotFoundError();
|
|
4283
4817
|
}
|
|
4284
4818
|
} else if (typeof result === "object") {
|
|
4285
4819
|
response = ctx.json(result);
|
|
@@ -4646,6 +5180,37 @@ function Event(eventName) {
|
|
|
4646
5180
|
function RateLimit(options) {
|
|
4647
5181
|
return Use(RateLimitMiddleware(options));
|
|
4648
5182
|
}
|
|
5183
|
+
function Tool(options) {
|
|
5184
|
+
return (target, propertyKey, descriptor) => {
|
|
5185
|
+
target[$mcpTools] ??= /* @__PURE__ */ new Map();
|
|
5186
|
+
target[$mcpTools].set(propertyKey, {
|
|
5187
|
+
name: options?.name,
|
|
5188
|
+
description: options?.description,
|
|
5189
|
+
inputSchema: options?.inputSchema
|
|
5190
|
+
});
|
|
5191
|
+
};
|
|
5192
|
+
}
|
|
5193
|
+
function Prompt(options) {
|
|
5194
|
+
return (target, propertyKey, descriptor) => {
|
|
5195
|
+
target[$mcpPrompts] ??= /* @__PURE__ */ new Map();
|
|
5196
|
+
target[$mcpPrompts].set(propertyKey, {
|
|
5197
|
+
name: options?.name,
|
|
5198
|
+
description: options?.description,
|
|
5199
|
+
arguments: options?.arguments
|
|
5200
|
+
});
|
|
5201
|
+
};
|
|
5202
|
+
}
|
|
5203
|
+
function Resource(uri, options) {
|
|
5204
|
+
return (target, propertyKey, descriptor) => {
|
|
5205
|
+
target[$mcpResources] ??= /* @__PURE__ */ new Map();
|
|
5206
|
+
target[$mcpResources].set(propertyKey, {
|
|
5207
|
+
uri,
|
|
5208
|
+
name: options?.name,
|
|
5209
|
+
description: options?.description,
|
|
5210
|
+
mimeType: options?.mimeType
|
|
5211
|
+
});
|
|
5212
|
+
};
|
|
5213
|
+
}
|
|
4649
5214
|
function ApiExplorerApp({ spec, base, asyncSpec, config }) {
|
|
4650
5215
|
const hierarchy = /* @__PURE__ */ new Map();
|
|
4651
5216
|
const addRoute = (groupKey, route) => {
|
|
@@ -5277,7 +5842,7 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
5277
5842
|
let astMiddlewareRegistry = {};
|
|
5278
5843
|
let applications = [];
|
|
5279
5844
|
try {
|
|
5280
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-
|
|
5845
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BOtveWL-.cjs"));
|
|
5281
5846
|
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
5282
5847
|
const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
5283
5848
|
const analysisResult = await analyzer2.analyze();
|
|
@@ -7549,6 +8114,463 @@ class Dashboard {
|
|
|
7549
8114
|
function unknownError(ctx) {
|
|
7550
8115
|
return ctx.json({ error: "Unknown Error" }, 500);
|
|
7551
8116
|
}
|
|
8117
|
+
let isPatched = false;
|
|
8118
|
+
function applyMonkeyPatch() {
|
|
8119
|
+
if (isPatched) return;
|
|
8120
|
+
isPatched = true;
|
|
8121
|
+
Error.stackTraceLimit = 50;
|
|
8122
|
+
}
|
|
8123
|
+
async function readSourceContext(filePath, line, contextLines = 5) {
|
|
8124
|
+
if (!filePath || filePath.startsWith("node:") || filePath.startsWith("bun:") || filePath.includes("node_modules")) {
|
|
8125
|
+
return null;
|
|
8126
|
+
}
|
|
8127
|
+
const path2 = filePath.startsWith("file://") ? filePath.slice(7) : filePath;
|
|
8128
|
+
try {
|
|
8129
|
+
const f = bun.file(path2);
|
|
8130
|
+
if (!await f.exists()) return null;
|
|
8131
|
+
const content = await f.text();
|
|
8132
|
+
const allLines = content.split("\n");
|
|
8133
|
+
const targetIndex = line - 1;
|
|
8134
|
+
if (targetIndex < 0 || targetIndex >= allLines.length) return null;
|
|
8135
|
+
const start = Math.max(0, targetIndex - contextLines);
|
|
8136
|
+
const end = Math.min(allLines.length, targetIndex + contextLines + 1);
|
|
8137
|
+
const subset = allLines.slice(start, end).map((code, i) => ({
|
|
8138
|
+
line: start + i + 1,
|
|
8139
|
+
code,
|
|
8140
|
+
isTarget: start + i + 1 === line
|
|
8141
|
+
}));
|
|
8142
|
+
return {
|
|
8143
|
+
lines: subset,
|
|
8144
|
+
startLine: start + 1,
|
|
8145
|
+
file: path2
|
|
8146
|
+
};
|
|
8147
|
+
} catch (e) {
|
|
8148
|
+
return null;
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
async function renderErrorView(ctx, error) {
|
|
8152
|
+
const frames = [];
|
|
8153
|
+
const cwd = process.cwd();
|
|
8154
|
+
const errorName = error?.name || "Error";
|
|
8155
|
+
const errorMessage = error?.message || "Unknown error occurred";
|
|
8156
|
+
const errorId = error?.id || ctx.requestId || "unknown-id";
|
|
8157
|
+
const errorTimestamp = error?.timestamp ? new Date(error.timestamp).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
8158
|
+
const errorScope = error?.scope || {};
|
|
8159
|
+
const lines = (error?.stack || "").split("\n").slice(1);
|
|
8160
|
+
for (const line of lines) {
|
|
8161
|
+
const match = line.match(/at (?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+))\)?/);
|
|
8162
|
+
if (match) {
|
|
8163
|
+
const [_, method, file, lineNo, colNo] = match;
|
|
8164
|
+
const fileName = file || "";
|
|
8165
|
+
let relativeFile = fileName;
|
|
8166
|
+
if (fileName.startsWith(cwd)) {
|
|
8167
|
+
relativeFile = fileName.slice(cwd.length + 1);
|
|
8168
|
+
}
|
|
8169
|
+
let isInternal = fileName.startsWith("node:") || fileName.startsWith("bun:") || fileName === "undefined" || fileName === "";
|
|
8170
|
+
if (isInternal && (method.includes("setTimeout") || method.includes("setInterval") || method.includes("setImmediate"))) {
|
|
8171
|
+
isInternal = false;
|
|
8172
|
+
}
|
|
8173
|
+
let isShokupan = false;
|
|
8174
|
+
if (fileName.includes("node_modules/@dotglitch/shokupan")) {
|
|
8175
|
+
isShokupan = true;
|
|
8176
|
+
} else if (relativeFile.startsWith("src/") || fileName.includes("/shokupan/dist/")) {
|
|
8177
|
+
isShokupan = true;
|
|
8178
|
+
}
|
|
8179
|
+
const isDependency = fileName.includes("node_modules") && !isShokupan;
|
|
8180
|
+
frames.push({
|
|
8181
|
+
method: method || "<anonymous>",
|
|
8182
|
+
file: fileName,
|
|
8183
|
+
line: parseInt(lineNo),
|
|
8184
|
+
column: parseInt(colNo),
|
|
8185
|
+
isNative: false,
|
|
8186
|
+
isInternal,
|
|
8187
|
+
isShokupan,
|
|
8188
|
+
isDependency,
|
|
8189
|
+
shortFile: fileName.split("/").pop() || fileName,
|
|
8190
|
+
relativeFile
|
|
8191
|
+
});
|
|
8192
|
+
}
|
|
8193
|
+
}
|
|
8194
|
+
let focusFrame = frames.find((f) => !f.isInternal && !f.isShokupan && !f.isDependency && !f.isNative);
|
|
8195
|
+
if (!focusFrame) focusFrame = frames[0];
|
|
8196
|
+
let sourceContext = null;
|
|
8197
|
+
if (focusFrame && focusFrame.file && !focusFrame.isInternal) {
|
|
8198
|
+
sourceContext = await readSourceContext(focusFrame.file, focusFrame.line, 8);
|
|
8199
|
+
}
|
|
8200
|
+
const renderFrames = frames.map((frame, index) => {
|
|
8201
|
+
const classes = [
|
|
8202
|
+
"stack-entry",
|
|
8203
|
+
frame.isInternal ? "internal" : "",
|
|
8204
|
+
frame.isShokupan ? "shokupan" : "",
|
|
8205
|
+
frame.isDependency ? "dependency" : "",
|
|
8206
|
+
frame === focusFrame ? "active" : ""
|
|
8207
|
+
].join(" ");
|
|
8208
|
+
const fileLink = `vscode://file/${frame.file}:${frame.line}:${frame.column}`;
|
|
8209
|
+
return `
|
|
8210
|
+
<li class="${classes}">
|
|
8211
|
+
<a href="${fileLink}" style="text-decoration:none; color:inherit; display:block">
|
|
8212
|
+
<div class="stack-method">${frame.method === "<anonymous>" ? "Anonymous" : frame.method}</div>
|
|
8213
|
+
<div class="stack-file">${frame.relativeFile}:${frame.line}</div>
|
|
8214
|
+
</a>
|
|
8215
|
+
</li>
|
|
8216
|
+
`;
|
|
8217
|
+
}).join("");
|
|
8218
|
+
const highlightCode = (code) => {
|
|
8219
|
+
return code.replace(/</g, "<").replace(/>/g, ">").replace(/(")(.*?)(")/g, '<span style="color:#a5d6ff">$1$2$3</span>').replace(/(')(.*?)(')/g, '<span style="color:#a5d6ff">$1$2$3</span>').replace(/(`)(.*?)(`)/g, '<span style="color:#a5d6ff">$1$2$3</span>').replace(/\b(const|let|var|function|class|import|export|from|return|if|else|switch|case|default|break|continue|try|catch|finally|throw|new|async|await|interface|type|extends|implements|public|private|protected|static|readonly|true|false|null|undefined)\b/g, '<span style="color:#ff7b72">$1</span>').replace(/(=>|===|==|!=|!==|\|\||&&|\+|\-|\*|\/|%|\+\+|\-\-)/g, '<span style="color:#ff7b72">$1</span>').replace(/\b([A-Z][a-zA-Z0-9_]*)\b/g, '<span style="color:#79c0ff">$1</span>').replace(/\b([a-zA-Z0-9_]+)(?=\()/g, '<span style="color:#d2a8ff">$1</span>').replace(/(\/\/.*)/g, '<span style="color:#8b949e; font-style:italic">$1</span>');
|
|
8220
|
+
};
|
|
8221
|
+
if (sourceContext) {
|
|
8222
|
+
sourceContext.lines.map((l) => `
|
|
8223
|
+
<div class="code-line ${l.isTarget ? "target" : ""}">
|
|
8224
|
+
<div class="line-number">${l.line}</div>
|
|
8225
|
+
<div class="line-content">${highlightCode(l.code)}</div>
|
|
8226
|
+
</div>
|
|
8227
|
+
`).join("");
|
|
8228
|
+
}
|
|
8229
|
+
const renderKV = (data) => {
|
|
8230
|
+
if (!data || Object.keys(data).length === 0) return '<div style="color:var(--text-muted)">None</div>';
|
|
8231
|
+
return `<table class="kv-table">
|
|
8232
|
+
${Object.entries(data).map(([k, v]) => {
|
|
8233
|
+
let displayVal = String(v);
|
|
8234
|
+
let valClass = "";
|
|
8235
|
+
if (typeof v === "number") {
|
|
8236
|
+
valClass = "kv-val-number";
|
|
8237
|
+
} else if (typeof v === "boolean") {
|
|
8238
|
+
valClass = "kv-val-bool";
|
|
8239
|
+
} else if (typeof v === "object" && v !== null) {
|
|
8240
|
+
try {
|
|
8241
|
+
displayVal = JSON.stringify(v, null, 2);
|
|
8242
|
+
valClass = "kv-val-json";
|
|
8243
|
+
} catch (e) {
|
|
8244
|
+
displayVal = "[Circular]";
|
|
8245
|
+
}
|
|
8246
|
+
}
|
|
8247
|
+
return `
|
|
8248
|
+
<tr>
|
|
8249
|
+
<td class="kv-key">${k}</td>
|
|
8250
|
+
<td class="kv-val ${valClass}">${displayVal}</td>
|
|
8251
|
+
</tr>`;
|
|
8252
|
+
}).join("")}
|
|
8253
|
+
</table>`;
|
|
8254
|
+
};
|
|
8255
|
+
const ICON_COPY = `<svg class="icon" viewBox="0 0 24 24"><path d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z"/></svg>`;
|
|
8256
|
+
return `<!DOCTYPE html>
|
|
8257
|
+
<html lang="en">
|
|
8258
|
+
<head>
|
|
8259
|
+
<meta charset="UTF-8">
|
|
8260
|
+
<title>${errorName}: ${errorMessage}</title>
|
|
8261
|
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css" rel="stylesheet" />
|
|
8262
|
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-highlight/prism-line-highlight.min.css" rel="stylesheet" />
|
|
8263
|
+
<link href="/_shokupan/error-view/prismjs.theme.css" rel="stylesheet" />
|
|
8264
|
+
<link href="/_shokupan/error-view/styles.css" rel="stylesheet" />
|
|
8265
|
+
<link href="/_shokupan/error-view/theme.css" rel="stylesheet" />
|
|
8266
|
+
|
|
8267
|
+
</head>
|
|
8268
|
+
<body class="">
|
|
8269
|
+
|
|
8270
|
+
<div class="page">
|
|
8271
|
+
<!-- HEADER -->
|
|
8272
|
+
<header class="chapter-header">
|
|
8273
|
+
<div class="chapter-meta">
|
|
8274
|
+
<div class="meta-item">
|
|
8275
|
+
<span>${ctx.method}</span>
|
|
8276
|
+
</div>
|
|
8277
|
+
<div class="meta-item">
|
|
8278
|
+
<span>${ctx.url.pathname}</span>
|
|
8279
|
+
</div>
|
|
8280
|
+
<div class="meta-item">
|
|
8281
|
+
<span>${ctx.response.status || 500}</span>
|
|
8282
|
+
</div>
|
|
8283
|
+
<div class="meta-item" style="margin-left:auto">
|
|
8284
|
+
<span class="id-badge" onclick="copyText('${errorId}')" title="Copy ID">ID: ${errorId}</span>
|
|
8285
|
+
</div>
|
|
8286
|
+
</div>
|
|
8287
|
+
|
|
8288
|
+
<h1 class="error-title">${errorName}</h1>
|
|
8289
|
+
|
|
8290
|
+
<div class="error-message-container">
|
|
8291
|
+
<h2 class="error-message">${errorMessage}</h2>
|
|
8292
|
+
<button class="action-btn" onclick="copyText('${errorMessage.replace(/'/g, "\\'")}')" title="Copy Message" style="padding:4px 8px">
|
|
8293
|
+
${ICON_COPY}
|
|
8294
|
+
</button>
|
|
8295
|
+
</div>
|
|
8296
|
+
|
|
8297
|
+
<div class="actions-bar">
|
|
8298
|
+
<button class="action-btn" onclick="copyText()">
|
|
8299
|
+
${ICON_COPY} Copy Error
|
|
8300
|
+
</button>
|
|
8301
|
+
<button class="action-btn" onclick="document.getElementById('raw-modal').style.display='flex'">
|
|
8302
|
+
View Raw Error
|
|
8303
|
+
</button>
|
|
8304
|
+
</div>
|
|
8305
|
+
</header>
|
|
8306
|
+
|
|
8307
|
+
<!-- CODE FIGURE -->
|
|
8308
|
+
<section class="figure">
|
|
8309
|
+
<div class="figure-caption">
|
|
8310
|
+
${focusFrame ? `<a href="vscode://file${focusFrame.file}:${focusFrame.line}" style="color:var(--text-muted); text-decoration:none">${focusFrame ? focusFrame.relativeFile : sourceContext?.file || "Unknown Source"}</a>` : ""}
|
|
8311
|
+
</div>
|
|
8312
|
+
<div class="figure-body">
|
|
8313
|
+
${sourceContext ? `
|
|
8314
|
+
<pre class="line-numbers" data-line="${sourceContext.lines.find((l) => l.isTarget)?.line}" data-start="${sourceContext.lines[0].line}"><code class="language-typescript">${sourceContext.lines.map((l) => l.code.replace(/</g, "<").replace(/>/g, ">")).join("\n")}</code></pre>
|
|
8315
|
+
` : `<div style="padding: 2rem; color: var(--text-muted); text-align: center;">Source code not available.</div>`}
|
|
8316
|
+
</div>
|
|
8317
|
+
</section>
|
|
8318
|
+
|
|
8319
|
+
<!-- NARRATIVE STACK -->
|
|
8320
|
+
<section class="narrative">
|
|
8321
|
+
<div class="section-title">
|
|
8322
|
+
<span>Stack Trace</span>
|
|
8323
|
+
<div class="filter-group">
|
|
8324
|
+
<span class="badge" onclick="this.classList.toggle('active'); document.body.classList.toggle('show-internals')">Internals</span>
|
|
8325
|
+
<span class="badge" onclick="this.classList.toggle('active'); document.body.classList.toggle('show-shokupan')">Framework</span>
|
|
8326
|
+
<span class="badge" onclick="this.classList.toggle('active'); document.body.classList.toggle('show-dependencies')">Dependencies</span>
|
|
8327
|
+
</div>
|
|
8328
|
+
</div>
|
|
8329
|
+
<ul class="stack-list">
|
|
8330
|
+
${renderFrames}
|
|
8331
|
+
</ul>
|
|
8332
|
+
</section>
|
|
8333
|
+
|
|
8334
|
+
<!-- APPENDICES -->
|
|
8335
|
+
<section class="appendix">
|
|
8336
|
+
<div class="section-title">Context & Environment</div>
|
|
8337
|
+
<div class="appendix-grid">
|
|
8338
|
+
<div class="data-block">
|
|
8339
|
+
<h3>Request</h3>
|
|
8340
|
+
${renderKV({
|
|
8341
|
+
id: errorId,
|
|
8342
|
+
timestamp: errorTimestamp,
|
|
8343
|
+
...errorScope || {}
|
|
8344
|
+
})}
|
|
8345
|
+
</div>
|
|
8346
|
+
<div class="data-block">
|
|
8347
|
+
<h3>Headers</h3>
|
|
8348
|
+
${renderKV(Object.fromEntries(ctx.headers))}
|
|
8349
|
+
</div>
|
|
8350
|
+
<div class="data-block">
|
|
8351
|
+
<h3>Query & Params</h3>
|
|
8352
|
+
${renderKV({ ...ctx.params, ...ctx.query })}
|
|
8353
|
+
</div>
|
|
8354
|
+
</div>
|
|
8355
|
+
</section>
|
|
8356
|
+
</div>
|
|
8357
|
+
|
|
8358
|
+
<!-- RAW ERROR MODAL -->
|
|
8359
|
+
<div id="raw-modal" class="modal-overlay" onclick="if(event.target === this) this.style.display='none'">
|
|
8360
|
+
<div class="modal-content">
|
|
8361
|
+
<div class="modal-header">
|
|
8362
|
+
<span>Raw Error Object</span>
|
|
8363
|
+
<button class="action-btn" onclick="document.getElementById('raw-modal').style.display='none'">Close</button>
|
|
8364
|
+
</div>
|
|
8365
|
+
<div class="modal-body" id="raw-content"></div>
|
|
8366
|
+
</div>
|
|
8367
|
+
</div>
|
|
8368
|
+
|
|
8369
|
+
<!-- PrismJS Scripts -->
|
|
8370
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"><\/script>
|
|
8371
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"><\/script>
|
|
8372
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"><\/script>
|
|
8373
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-highlight/prism-line-highlight.min.js"><\/script>
|
|
8374
|
+
|
|
8375
|
+
<script>
|
|
8376
|
+
// Prepare Raw Error
|
|
8377
|
+
// circular ref safe stringify
|
|
8378
|
+
const getCircularReplacer = () => {
|
|
8379
|
+
const seen = new WeakSet();
|
|
8380
|
+
return (key, value) => {
|
|
8381
|
+
if (typeof value === "object" && value !== null) {
|
|
8382
|
+
if (seen.has(value)) {
|
|
8383
|
+
return "[Circular]";
|
|
8384
|
+
}
|
|
8385
|
+
seen.add(value);
|
|
8386
|
+
}
|
|
8387
|
+
return value;
|
|
8388
|
+
};
|
|
8389
|
+
};
|
|
8390
|
+
|
|
8391
|
+
// Inject error data from SERVER side
|
|
8392
|
+
const rawError = ${(() => {
|
|
8393
|
+
const serializeError = (err) => {
|
|
8394
|
+
const obj = {
|
|
8395
|
+
name: err.name,
|
|
8396
|
+
message: err.message,
|
|
8397
|
+
stack: err.stack,
|
|
8398
|
+
...err
|
|
8399
|
+
// Spread enumerable props
|
|
8400
|
+
};
|
|
8401
|
+
if (err.cause) obj.cause = err.cause;
|
|
8402
|
+
if (err.code) obj.code = err.code;
|
|
8403
|
+
if (err.status) obj.status = err.status;
|
|
8404
|
+
if (err.statusCode) obj.statusCode = err.statusCode;
|
|
8405
|
+
return JSON.stringify(obj, (key, value) => {
|
|
8406
|
+
if (key === "structuredStack") return void 0;
|
|
8407
|
+
return value;
|
|
8408
|
+
}, 2);
|
|
8409
|
+
};
|
|
8410
|
+
return serializeError(error);
|
|
8411
|
+
})()};
|
|
8412
|
+
|
|
8413
|
+
// At this point 'rawError' is an Object in Client JS (because serializeError returned a JSON string)
|
|
8414
|
+
const RAW_ERROR_JSON = JSON.stringify(rawError, getCircularReplacer(), 2);
|
|
8415
|
+
// "Normally printed" usually means standard stacktrace string which includes name/message
|
|
8416
|
+
const RAW_ERROR_TEXT = rawError.stack || (rawError.name + ': ' + rawError.message);
|
|
8417
|
+
|
|
8418
|
+
document.getElementById('raw-content').innerText = RAW_ERROR_JSON;
|
|
8419
|
+
|
|
8420
|
+
function copyText(text) {
|
|
8421
|
+
if (!text) text = RAW_ERROR_TEXT; // Default to text representation (Message + Stack)
|
|
8422
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
8423
|
+
console.log('Copied');
|
|
8424
|
+
});
|
|
8425
|
+
}
|
|
8426
|
+
<\/script>
|
|
8427
|
+
</body>
|
|
8428
|
+
</html>`;
|
|
8429
|
+
}
|
|
8430
|
+
function renderStatusView(ctx, status, error) {
|
|
8431
|
+
const title = `${status} ${error.message || "Error"}`;
|
|
8432
|
+
const method = ctx.method;
|
|
8433
|
+
const path2 = ctx.url.pathname;
|
|
8434
|
+
const css = `
|
|
8435
|
+
body {
|
|
8436
|
+
background: var(--bg-primary);
|
|
8437
|
+
color: var(--text-primary);
|
|
8438
|
+
font-family: var(--shokupan-font);
|
|
8439
|
+
display: flex;
|
|
8440
|
+
align-items: center;
|
|
8441
|
+
justify-content: center;
|
|
8442
|
+
height: 100vh;
|
|
8443
|
+
margin: 0;
|
|
8444
|
+
overflow: hidden;
|
|
8445
|
+
}
|
|
8446
|
+
.container {
|
|
8447
|
+
text-align: center;
|
|
8448
|
+
animation: fadeIn 0.3s ease-out;
|
|
8449
|
+
background: var(--bg-card);
|
|
8450
|
+
padding: 3rem 4rem;
|
|
8451
|
+
border-radius: 16px;
|
|
8452
|
+
border: 1px solid var(--card-border);
|
|
8453
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
8454
|
+
max-width: 600px;
|
|
8455
|
+
}
|
|
8456
|
+
h1 {
|
|
8457
|
+
font-size: 6rem;
|
|
8458
|
+
margin: 0;
|
|
8459
|
+
color: var(--primary);
|
|
8460
|
+
line-height: 1;
|
|
8461
|
+
font-weight: 800;
|
|
8462
|
+
letter-spacing: -2px;
|
|
8463
|
+
text-shadow: 0 4px 20px rgba(255, 179, 128, 0.2);
|
|
8464
|
+
}
|
|
8465
|
+
h2 {
|
|
8466
|
+
font-size: 1.5rem;
|
|
8467
|
+
margin: 1rem 0 2rem 0;
|
|
8468
|
+
font-weight: 400;
|
|
8469
|
+
color: var(--text-secondary);
|
|
8470
|
+
}
|
|
8471
|
+
.meta {
|
|
8472
|
+
color: var(--text-muted);
|
|
8473
|
+
font-family: var(--shokupan-font-mono);
|
|
8474
|
+
font-size: 1rem;
|
|
8475
|
+
background: var(--bg-primary);
|
|
8476
|
+
padding: 0.75rem 1.5rem;
|
|
8477
|
+
border-radius: 8px;
|
|
8478
|
+
display: inline-block;
|
|
8479
|
+
border: 1px solid var(--border-color);
|
|
8480
|
+
}
|
|
8481
|
+
.method {
|
|
8482
|
+
font-weight: bold;
|
|
8483
|
+
margin-right: 0.5rem;
|
|
8484
|
+
padding: 0.2rem 0.5rem;
|
|
8485
|
+
border-radius: 4px;
|
|
8486
|
+
}
|
|
8487
|
+
.path {
|
|
8488
|
+
color: var(--text-primary);
|
|
8489
|
+
}
|
|
8490
|
+
@keyframes fadeIn {
|
|
8491
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
8492
|
+
to { opacity: 1; transform: translateY(0); }
|
|
8493
|
+
}
|
|
8494
|
+
`;
|
|
8495
|
+
return `<!DOCTYPE html>
|
|
8496
|
+
<html lang="en">
|
|
8497
|
+
<head>
|
|
8498
|
+
<meta charset="UTF-8">
|
|
8499
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8500
|
+
<title>${title}</title>
|
|
8501
|
+
<link href="/_shokupan/error-view/theme.css" rel="stylesheet" />
|
|
8502
|
+
<style>${css}</style>
|
|
8503
|
+
</head>
|
|
8504
|
+
<body>
|
|
8505
|
+
<div class="container">
|
|
8506
|
+
<h1>${status}</h1>
|
|
8507
|
+
<h2>${error.message || "An error occurred"}</h2>
|
|
8508
|
+
<div class="meta">
|
|
8509
|
+
<span class="method badge-${method}">${method}</span>
|
|
8510
|
+
<span class="path">${path2}</span>
|
|
8511
|
+
</div>
|
|
8512
|
+
</div>
|
|
8513
|
+
</body>
|
|
8514
|
+
</html>`;
|
|
8515
|
+
}
|
|
8516
|
+
class ErrorView {
|
|
8517
|
+
constructor(config = {}) {
|
|
8518
|
+
this.config = config;
|
|
8519
|
+
}
|
|
8520
|
+
name = "error-view";
|
|
8521
|
+
async onInit(app) {
|
|
8522
|
+
applyMonkeyPatch();
|
|
8523
|
+
const errorViewMiddleware = async (ctx, next) => {
|
|
8524
|
+
try {
|
|
8525
|
+
return await next();
|
|
8526
|
+
} catch (err) {
|
|
8527
|
+
const accept = ctx.get("accept") || "";
|
|
8528
|
+
if (!accept.includes("text/html")) {
|
|
8529
|
+
throw err;
|
|
8530
|
+
}
|
|
8531
|
+
if (!err.timestamp) {
|
|
8532
|
+
Object.defineProperty(err, "timestamp", {
|
|
8533
|
+
value: Date.now(),
|
|
8534
|
+
enumerable: false,
|
|
8535
|
+
writable: true,
|
|
8536
|
+
configurable: true
|
|
8537
|
+
});
|
|
8538
|
+
}
|
|
8539
|
+
if (!err.id) {
|
|
8540
|
+
Object.defineProperty(err, "id", {
|
|
8541
|
+
value: ctx.requestId,
|
|
8542
|
+
enumerable: false,
|
|
8543
|
+
writable: true,
|
|
8544
|
+
configurable: true
|
|
8545
|
+
});
|
|
8546
|
+
}
|
|
8547
|
+
if (!err.scope) {
|
|
8548
|
+
const store = asyncContext.getStore();
|
|
8549
|
+
if (store) {
|
|
8550
|
+
Object.defineProperty(err, "scope", {
|
|
8551
|
+
value: { ...store },
|
|
8552
|
+
enumerable: false,
|
|
8553
|
+
writable: true,
|
|
8554
|
+
configurable: true
|
|
8555
|
+
});
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8558
|
+
const status = getErrorStatus(err);
|
|
8559
|
+
if (status === 404 || status === 401 || status === 403) {
|
|
8560
|
+
const html2 = await renderStatusView(ctx, status, err);
|
|
8561
|
+
return ctx.html(html2, status);
|
|
8562
|
+
}
|
|
8563
|
+
const html = await renderErrorView(ctx, err);
|
|
8564
|
+
return ctx.html(html, status);
|
|
8565
|
+
}
|
|
8566
|
+
};
|
|
8567
|
+
Object.defineProperty(errorViewMiddleware, "name", { value: "ErrorViewMiddleware" });
|
|
8568
|
+
const { join } = await import("path");
|
|
8569
|
+
const assetDir = join(void 0, "assets");
|
|
8570
|
+
app.static("/_shokupan/error-view", assetDir);
|
|
8571
|
+
app.use(errorViewMiddleware);
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
7552
8574
|
class GraphQLApolloPlugin extends ShokupanRouter {
|
|
7553
8575
|
// Use generic any or verify type
|
|
7554
8576
|
constructor(pluginOptions) {
|
|
@@ -7623,6 +8645,377 @@ class GraphQLApolloPlugin extends ShokupanRouter {
|
|
|
7623
8645
|
});
|
|
7624
8646
|
}
|
|
7625
8647
|
}
|
|
8648
|
+
class GraphQLYogaPlugin extends ShokupanRouter {
|
|
8649
|
+
constructor(pluginOptions) {
|
|
8650
|
+
super();
|
|
8651
|
+
this.pluginOptions = pluginOptions;
|
|
8652
|
+
this.pluginOptions.path ??= "/graphql";
|
|
8653
|
+
}
|
|
8654
|
+
yoga;
|
|
8655
|
+
async onInit(app, options) {
|
|
8656
|
+
const { createYoga } = await import("graphql-yoga");
|
|
8657
|
+
const path2 = options?.path || this.pluginOptions.path || "/graphql";
|
|
8658
|
+
this.yoga = createYoga({
|
|
8659
|
+
...this.pluginOptions.yogaConfig,
|
|
8660
|
+
graphqlEndpoint: path2
|
|
8661
|
+
});
|
|
8662
|
+
app.mount(path2, this);
|
|
8663
|
+
const handler = async (ctx) => {
|
|
8664
|
+
let body;
|
|
8665
|
+
if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") {
|
|
8666
|
+
body = await ctx.body();
|
|
8667
|
+
if (typeof body === "object" && body !== null) {
|
|
8668
|
+
body = JSON.stringify(body);
|
|
8669
|
+
}
|
|
8670
|
+
}
|
|
8671
|
+
const response = await this.yoga.fetch(
|
|
8672
|
+
new Request(ctx.req.url, {
|
|
8673
|
+
method: ctx.req.method,
|
|
8674
|
+
headers: ctx.req.headers,
|
|
8675
|
+
body
|
|
8676
|
+
}),
|
|
8677
|
+
{
|
|
8678
|
+
...ctx
|
|
8679
|
+
}
|
|
8680
|
+
);
|
|
8681
|
+
response.headers.forEach((value, key) => {
|
|
8682
|
+
ctx.set(key, value);
|
|
8683
|
+
});
|
|
8684
|
+
const text = await response.text();
|
|
8685
|
+
return ctx.send(text, {
|
|
8686
|
+
status: response.status
|
|
8687
|
+
});
|
|
8688
|
+
};
|
|
8689
|
+
this.get("/", handler);
|
|
8690
|
+
this.post("/", handler);
|
|
8691
|
+
this.get("/*", handler);
|
|
8692
|
+
this.post("/*", handler);
|
|
8693
|
+
}
|
|
8694
|
+
}
|
|
8695
|
+
class HtmxPlugin {
|
|
8696
|
+
async onInit(app) {
|
|
8697
|
+
app.use(this.middleware());
|
|
8698
|
+
}
|
|
8699
|
+
middleware() {
|
|
8700
|
+
return async (ctx, next) => {
|
|
8701
|
+
Object.defineProperty(ctx, "isHtmx", {
|
|
8702
|
+
get: () => ctx.req.headers.has("hx-request")
|
|
8703
|
+
});
|
|
8704
|
+
Object.defineProperty(ctx, "isHtmxBoosted", {
|
|
8705
|
+
get: () => ctx.req.headers.has("hx-boosted")
|
|
8706
|
+
});
|
|
8707
|
+
ctx.trigger = (event, options) => {
|
|
8708
|
+
let headerName = "HX-Trigger";
|
|
8709
|
+
if (options?.after === "settle") headerName = "HX-Trigger-After-Settle";
|
|
8710
|
+
if (options?.after === "swap") headerName = "HX-Trigger-After-Swap";
|
|
8711
|
+
let value = JSON.stringify(event);
|
|
8712
|
+
if (typeof event === "string") {
|
|
8713
|
+
value = event;
|
|
8714
|
+
} else {
|
|
8715
|
+
value = JSON.stringify(event);
|
|
8716
|
+
}
|
|
8717
|
+
ctx.set(headerName, value);
|
|
8718
|
+
};
|
|
8719
|
+
ctx.pushUrl = (url) => {
|
|
8720
|
+
ctx.set("HX-Push-Url", url === false ? "false" : url);
|
|
8721
|
+
};
|
|
8722
|
+
ctx.htmxRedirect = (url) => {
|
|
8723
|
+
ctx.set("HX-Redirect", url);
|
|
8724
|
+
};
|
|
8725
|
+
ctx.refresh = () => {
|
|
8726
|
+
ctx.set("HX-Refresh", "true");
|
|
8727
|
+
};
|
|
8728
|
+
return next();
|
|
8729
|
+
};
|
|
8730
|
+
}
|
|
8731
|
+
}
|
|
8732
|
+
function Idempotency(options = {}) {
|
|
8733
|
+
const headerName = options.header || "Idempotency-Key";
|
|
8734
|
+
options.ttl || 24 * 60 * 60 * 1e3;
|
|
8735
|
+
let RecordIdClass;
|
|
8736
|
+
const idempotencyMiddleware = async function IdempotencyMiddleware(ctx, next) {
|
|
8737
|
+
const key = ctx.headers.get(headerName);
|
|
8738
|
+
if (!key) {
|
|
8739
|
+
return next();
|
|
8740
|
+
}
|
|
8741
|
+
try {
|
|
8742
|
+
if (!RecordIdClass) {
|
|
8743
|
+
const mod = await import("surrealdb");
|
|
8744
|
+
RecordIdClass = mod.RecordId;
|
|
8745
|
+
}
|
|
8746
|
+
const stored = await ctx.app.db.select(new RecordIdClass("idempotency", key));
|
|
8747
|
+
if (stored) {
|
|
8748
|
+
const responseHeaders = new Headers(stored.headers);
|
|
8749
|
+
responseHeaders.set("X-Idempotency-Hit", "true");
|
|
8750
|
+
return new Response(stored.body, {
|
|
8751
|
+
status: stored.status,
|
|
8752
|
+
headers: responseHeaders
|
|
8753
|
+
});
|
|
8754
|
+
}
|
|
8755
|
+
} catch (e) {
|
|
8756
|
+
console.error("Idempotency read error:", e);
|
|
8757
|
+
}
|
|
8758
|
+
const result = await next();
|
|
8759
|
+
let response;
|
|
8760
|
+
if (result instanceof Response) {
|
|
8761
|
+
response = result;
|
|
8762
|
+
} else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
|
|
8763
|
+
response = ctx[$finalResponse];
|
|
8764
|
+
} else if (result !== null && result !== void 0) {
|
|
8765
|
+
if (typeof result === "object") {
|
|
8766
|
+
response = await ctx.json(result);
|
|
8767
|
+
} else {
|
|
8768
|
+
response = await ctx.text(String(result));
|
|
8769
|
+
}
|
|
8770
|
+
}
|
|
8771
|
+
if (response instanceof Response) {
|
|
8772
|
+
const clone = response.clone();
|
|
8773
|
+
const bodyText = await clone.text();
|
|
8774
|
+
const headers = {};
|
|
8775
|
+
clone.headers.forEach((v, k) => {
|
|
8776
|
+
headers[k] = v;
|
|
8777
|
+
});
|
|
8778
|
+
const toStore = {
|
|
8779
|
+
status: clone.status,
|
|
8780
|
+
headers,
|
|
8781
|
+
body: bodyText,
|
|
8782
|
+
timestamp: Date.now()
|
|
8783
|
+
};
|
|
8784
|
+
try {
|
|
8785
|
+
await ctx.app.db.upsert(new RecordIdClass("idempotency", key), toStore);
|
|
8786
|
+
} catch (e) {
|
|
8787
|
+
console.error("Idempotency write error:", e);
|
|
8788
|
+
}
|
|
8789
|
+
return response;
|
|
8790
|
+
}
|
|
8791
|
+
return result;
|
|
8792
|
+
};
|
|
8793
|
+
idempotencyMiddleware.isBuiltin = true;
|
|
8794
|
+
idempotencyMiddleware.pluginName = "Idempotency";
|
|
8795
|
+
return idempotencyMiddleware;
|
|
8796
|
+
}
|
|
8797
|
+
class MCPServerPlugin {
|
|
8798
|
+
constructor(options = {}) {
|
|
8799
|
+
this.options = options;
|
|
8800
|
+
options.allowIntrospection ??= true;
|
|
8801
|
+
options.allowToolExecution ??= true;
|
|
8802
|
+
options.path ??= "/mcp";
|
|
8803
|
+
if (!options.path.startsWith("/")) {
|
|
8804
|
+
options.path = "/" + options.path;
|
|
8805
|
+
}
|
|
8806
|
+
options.rootDir ??= process.cwd();
|
|
8807
|
+
}
|
|
8808
|
+
router = new ShokupanRouter();
|
|
8809
|
+
analyzer;
|
|
8810
|
+
onInit(app) {
|
|
8811
|
+
this[$appRoot] = app;
|
|
8812
|
+
this.analyzer = new analyzer_impl.OpenAPIAnalyzer(this.options.rootDir);
|
|
8813
|
+
if (this.options.allowIntrospection) {
|
|
8814
|
+
this.registerTools();
|
|
8815
|
+
this.registerResources();
|
|
8816
|
+
this.registerPrompts();
|
|
8817
|
+
}
|
|
8818
|
+
app.onStart(async () => {
|
|
8819
|
+
app.mount(this.options.path, this.router);
|
|
8820
|
+
this.collectAppMcpItems(app);
|
|
8821
|
+
this.setupRoutes();
|
|
8822
|
+
this.router.metadata = {
|
|
8823
|
+
file: void 0,
|
|
8824
|
+
line: 1,
|
|
8825
|
+
name: "MCPServerPlugin",
|
|
8826
|
+
pluginName: "MCP Server"
|
|
8827
|
+
};
|
|
8828
|
+
});
|
|
8829
|
+
}
|
|
8830
|
+
collectAppMcpItems(app) {
|
|
8831
|
+
const collect = (router) => {
|
|
8832
|
+
if (router.mcpProtocol) {
|
|
8833
|
+
this.router.mcpProtocol.merge(router.mcpProtocol);
|
|
8834
|
+
}
|
|
8835
|
+
router[$childRouters]?.forEach(collect);
|
|
8836
|
+
};
|
|
8837
|
+
collect(app);
|
|
8838
|
+
}
|
|
8839
|
+
setupRoutes() {
|
|
8840
|
+
this.router.get("", (ctx) => {
|
|
8841
|
+
const endpointUrl = `${ctx.protocol}://${ctx.host}${this.options.path}`;
|
|
8842
|
+
const enc = new TextEncoder();
|
|
8843
|
+
return new Response(
|
|
8844
|
+
new ReadableStream({
|
|
8845
|
+
start(controller) {
|
|
8846
|
+
controller.enqueue(enc.encode(`event: endpoint
|
|
8847
|
+
data: ${JSON.stringify(endpointUrl)}
|
|
8848
|
+
|
|
8849
|
+
`));
|
|
8850
|
+
},
|
|
8851
|
+
cancel() {
|
|
8852
|
+
}
|
|
8853
|
+
}),
|
|
8854
|
+
{
|
|
8855
|
+
headers: {
|
|
8856
|
+
"Content-Type": "text/event-stream",
|
|
8857
|
+
"Cache-Control": "no-cache",
|
|
8858
|
+
"Connection": "keep-alive"
|
|
8859
|
+
}
|
|
8860
|
+
}
|
|
8861
|
+
);
|
|
8862
|
+
});
|
|
8863
|
+
this.router.post("", async (ctx) => {
|
|
8864
|
+
let parsedBody;
|
|
8865
|
+
try {
|
|
8866
|
+
parsedBody = await ctx.body();
|
|
8867
|
+
} catch (e) {
|
|
8868
|
+
return ctx.json({
|
|
8869
|
+
jsonrpc: "2.0",
|
|
8870
|
+
id: null,
|
|
8871
|
+
error: { code: -32700, message: "Parse error" }
|
|
8872
|
+
}, 400);
|
|
8873
|
+
}
|
|
8874
|
+
const response = await this.router.mcpProtocol.handleMessage(parsedBody);
|
|
8875
|
+
if (response) {
|
|
8876
|
+
return ctx.json(response);
|
|
8877
|
+
}
|
|
8878
|
+
return ctx.text("", 204);
|
|
8879
|
+
});
|
|
8880
|
+
}
|
|
8881
|
+
registerTools() {
|
|
8882
|
+
const ensureExecutionAllowed = () => {
|
|
8883
|
+
if (!this.options.allowToolExecution) {
|
|
8884
|
+
throw new Error("Tool execution is disabled.");
|
|
8885
|
+
}
|
|
8886
|
+
};
|
|
8887
|
+
this.router.tool(
|
|
8888
|
+
"list_endpoints",
|
|
8889
|
+
{},
|
|
8890
|
+
async () => {
|
|
8891
|
+
ensureExecutionAllowed();
|
|
8892
|
+
const { applications } = await this.analyzer.analyze();
|
|
8893
|
+
const endpoints = applications.flatMap(
|
|
8894
|
+
(app) => app.routes.map((r) => ({
|
|
8895
|
+
method: r.method,
|
|
8896
|
+
path: r.path,
|
|
8897
|
+
handler: r.handlerName,
|
|
8898
|
+
summary: r.summary
|
|
8899
|
+
}))
|
|
8900
|
+
);
|
|
8901
|
+
return {
|
|
8902
|
+
content: [{
|
|
8903
|
+
type: "text",
|
|
8904
|
+
text: JSON.stringify(endpoints, null, 2)
|
|
8905
|
+
}]
|
|
8906
|
+
};
|
|
8907
|
+
}
|
|
8908
|
+
);
|
|
8909
|
+
this.router.tool(
|
|
8910
|
+
"get_endpoint_details",
|
|
8911
|
+
{
|
|
8912
|
+
type: "object",
|
|
8913
|
+
properties: {
|
|
8914
|
+
method: { type: "string" },
|
|
8915
|
+
path: { type: "string" }
|
|
8916
|
+
},
|
|
8917
|
+
required: ["method", "path"]
|
|
8918
|
+
},
|
|
8919
|
+
async ({ method, path: path2 }) => {
|
|
8920
|
+
ensureExecutionAllowed();
|
|
8921
|
+
const { applications } = await this.analyzer.analyze();
|
|
8922
|
+
const route = applications.flatMap((app) => app.routes).find((r) => r.method.toUpperCase() === method.toUpperCase() && r.path === path2);
|
|
8923
|
+
if (!route) {
|
|
8924
|
+
return {
|
|
8925
|
+
content: [{ type: "text", text: `Endpoint ${method} ${path2} not found.` }],
|
|
8926
|
+
isError: true
|
|
8927
|
+
};
|
|
8928
|
+
}
|
|
8929
|
+
return {
|
|
8930
|
+
content: [{
|
|
8931
|
+
type: "text",
|
|
8932
|
+
text: JSON.stringify(route, null, 2)
|
|
8933
|
+
}]
|
|
8934
|
+
};
|
|
8935
|
+
}
|
|
8936
|
+
);
|
|
8937
|
+
}
|
|
8938
|
+
registerResources() {
|
|
8939
|
+
this.router.resource(
|
|
8940
|
+
"mcp://api/openapi.json",
|
|
8941
|
+
{
|
|
8942
|
+
name: "openapi-spec",
|
|
8943
|
+
mimeType: "application/json"
|
|
8944
|
+
},
|
|
8945
|
+
async (uri) => {
|
|
8946
|
+
const { applications } = await this.analyzer.analyze();
|
|
8947
|
+
const endpoints = applications.flatMap(
|
|
8948
|
+
(app) => app.routes.map((r) => ({
|
|
8949
|
+
method: r.method,
|
|
8950
|
+
path: r.path,
|
|
8951
|
+
handler: r.handlerName,
|
|
8952
|
+
summary: r.summary,
|
|
8953
|
+
requestTypes: r.requestTypes,
|
|
8954
|
+
responseType: r.responseType
|
|
8955
|
+
}))
|
|
8956
|
+
);
|
|
8957
|
+
return {
|
|
8958
|
+
contents: [{
|
|
8959
|
+
uri,
|
|
8960
|
+
text: JSON.stringify(endpoints, null, 2)
|
|
8961
|
+
}]
|
|
8962
|
+
};
|
|
8963
|
+
}
|
|
8964
|
+
);
|
|
8965
|
+
this.router.resource(
|
|
8966
|
+
"mcp://api/routes/{method}/{path}/source",
|
|
8967
|
+
{
|
|
8968
|
+
name: "route-source",
|
|
8969
|
+
mimeType: "text/typescript"
|
|
8970
|
+
},
|
|
8971
|
+
async (uri) => {
|
|
8972
|
+
const parts = uri.replace("mcp://", "").split("/");
|
|
8973
|
+
parts[2];
|
|
8974
|
+
throw new Error("Dynamic resource reading not fully implemented in lightweight version yet.");
|
|
8975
|
+
}
|
|
8976
|
+
);
|
|
8977
|
+
}
|
|
8978
|
+
registerPrompts() {
|
|
8979
|
+
this.router.prompt(
|
|
8980
|
+
"generate-client",
|
|
8981
|
+
[
|
|
8982
|
+
{ name: "method", required: true },
|
|
8983
|
+
{ name: "path", required: true }
|
|
8984
|
+
],
|
|
8985
|
+
async ({ method, path: path2 }) => {
|
|
8986
|
+
const { applications } = await this.analyzer.analyze();
|
|
8987
|
+
const route = applications.flatMap((app) => app.routes).find((r) => r.method.toUpperCase() === method.toUpperCase() && r.path === path2);
|
|
8988
|
+
if (!route) {
|
|
8989
|
+
return {
|
|
8990
|
+
messages: [{
|
|
8991
|
+
role: "user",
|
|
8992
|
+
content: {
|
|
8993
|
+
type: "text",
|
|
8994
|
+
text: `Start a new task to create a client for ${method} ${path2}. The endpoint was not found in the current analysis.`
|
|
8995
|
+
}
|
|
8996
|
+
}]
|
|
8997
|
+
};
|
|
8998
|
+
}
|
|
8999
|
+
return {
|
|
9000
|
+
messages: [{
|
|
9001
|
+
role: "user",
|
|
9002
|
+
content: {
|
|
9003
|
+
type: "text",
|
|
9004
|
+
text: `Please generate a TypeScript client function for the following endpoint:
|
|
9005
|
+
Method: ${route.method}
|
|
9006
|
+
Path: ${route.path}
|
|
9007
|
+
Summary: ${route.summary || "N/A"}
|
|
9008
|
+
Request Types: ${JSON.stringify(route.requestTypes, null, 2)}
|
|
9009
|
+
Response Type: ${route.responseType || "unknown"}
|
|
9010
|
+
|
|
9011
|
+
Use fetch or axios. Ensure proper typing.`
|
|
9012
|
+
}
|
|
9013
|
+
}]
|
|
9014
|
+
};
|
|
9015
|
+
}
|
|
9016
|
+
);
|
|
9017
|
+
}
|
|
9018
|
+
}
|
|
7626
9019
|
class ScalarPlugin extends ShokupanRouter {
|
|
7627
9020
|
constructor(pluginOptions = {}) {
|
|
7628
9021
|
pluginOptions.config ??= {};
|
|
@@ -7811,6 +9204,79 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
7811
9204
|
}
|
|
7812
9205
|
}
|
|
7813
9206
|
}
|
|
9207
|
+
function attachSocketIOBridge(io, app) {
|
|
9208
|
+
io.on("connection", (socket) => {
|
|
9209
|
+
socket.onAny(async (event, ...args) => {
|
|
9210
|
+
if (event === "shokupan:request" || event === "http") {
|
|
9211
|
+
return;
|
|
9212
|
+
}
|
|
9213
|
+
const handler = app.findEvent(event);
|
|
9214
|
+
if (handler) {
|
|
9215
|
+
const data = args[0];
|
|
9216
|
+
const req = new ShokupanRequest({
|
|
9217
|
+
url: `socketio://${app.applicationConfig.hostname || "localhost"}/event/${event}`,
|
|
9218
|
+
method: "POST",
|
|
9219
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
9220
|
+
body: JSON.stringify(data)
|
|
9221
|
+
});
|
|
9222
|
+
const ctx = new ShokupanContext(req, app.server);
|
|
9223
|
+
ctx[$ws] = socket;
|
|
9224
|
+
ctx.io = io;
|
|
9225
|
+
try {
|
|
9226
|
+
for (let i = 0; i < handler.length; i++) {
|
|
9227
|
+
await handler[i](ctx);
|
|
9228
|
+
}
|
|
9229
|
+
} catch (e) {
|
|
9230
|
+
await app.runHooks("onError", ctx, e);
|
|
9231
|
+
if (app.applicationConfig["websocketErrorHandler"]) {
|
|
9232
|
+
await app.applicationConfig["websocketErrorHandler"](e, ctx);
|
|
9233
|
+
} else {
|
|
9234
|
+
console.error(`Error in event ${event}:`, e);
|
|
9235
|
+
}
|
|
9236
|
+
}
|
|
9237
|
+
}
|
|
9238
|
+
});
|
|
9239
|
+
if (app.applicationConfig["enableHttpBridge"]) {
|
|
9240
|
+
socket.on("shokupan:request", async (payload, callback) => {
|
|
9241
|
+
try {
|
|
9242
|
+
const { method, path: path2, headers, body } = payload;
|
|
9243
|
+
const url = new URL(path2, `http://${app.applicationConfig.hostname || "localhost"}:3000`);
|
|
9244
|
+
const req = new Request(url.toString(), {
|
|
9245
|
+
method,
|
|
9246
|
+
headers,
|
|
9247
|
+
body: typeof body === "object" ? JSON.stringify(body) : body
|
|
9248
|
+
});
|
|
9249
|
+
const res = await app.fetch(req);
|
|
9250
|
+
let resBody = await res.text();
|
|
9251
|
+
try {
|
|
9252
|
+
resBody = JSON.parse(resBody);
|
|
9253
|
+
} catch {
|
|
9254
|
+
}
|
|
9255
|
+
const resHeaders = {};
|
|
9256
|
+
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
9257
|
+
if (typeof callback === "function") {
|
|
9258
|
+
await callback({
|
|
9259
|
+
status: res.status,
|
|
9260
|
+
headers: resHeaders,
|
|
9261
|
+
body: resBody
|
|
9262
|
+
});
|
|
9263
|
+
} else {
|
|
9264
|
+
socket.emit("shokupan:response", {
|
|
9265
|
+
id: payload.id,
|
|
9266
|
+
status: res.status,
|
|
9267
|
+
headers: resHeaders,
|
|
9268
|
+
body: resBody
|
|
9269
|
+
});
|
|
9270
|
+
}
|
|
9271
|
+
} catch (e) {
|
|
9272
|
+
if (typeof callback === "function") {
|
|
9273
|
+
callback({ status: 500, body: { error: e.message } });
|
|
9274
|
+
}
|
|
9275
|
+
}
|
|
9276
|
+
});
|
|
9277
|
+
}
|
|
9278
|
+
});
|
|
9279
|
+
}
|
|
7814
9280
|
function createLimitStream(maxSize) {
|
|
7815
9281
|
let size = 0;
|
|
7816
9282
|
return new TransformStream({
|
|
@@ -8453,6 +9919,168 @@ function enableOpenApiValidation(app) {
|
|
|
8453
9919
|
precompileValidators(app, spec);
|
|
8454
9920
|
});
|
|
8455
9921
|
}
|
|
9922
|
+
function isPrivateIP(ip) {
|
|
9923
|
+
const ipv4Patterns = [
|
|
9924
|
+
/^10\./,
|
|
9925
|
+
// 10.0.0.0/8
|
|
9926
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
9927
|
+
// 172.16.0.0/12
|
|
9928
|
+
/^192\.168\./,
|
|
9929
|
+
// 192.168.0.0/16
|
|
9930
|
+
/^127\./,
|
|
9931
|
+
// 127.0.0.0/8 (loopback)
|
|
9932
|
+
/^169\.254\./,
|
|
9933
|
+
// 169.254.0.0/16 (link-local)
|
|
9934
|
+
/^0\.0\.0\.0$/
|
|
9935
|
+
// 0.0.0.0
|
|
9936
|
+
];
|
|
9937
|
+
const ipv6Patterns = [
|
|
9938
|
+
/^::1$/,
|
|
9939
|
+
// loopback
|
|
9940
|
+
/^fe80:/,
|
|
9941
|
+
// link-local
|
|
9942
|
+
/^fc00:/,
|
|
9943
|
+
// unique local
|
|
9944
|
+
/^fd00:/
|
|
9945
|
+
// unique local
|
|
9946
|
+
];
|
|
9947
|
+
for (const pattern of ipv4Patterns) {
|
|
9948
|
+
if (pattern.test(ip)) return true;
|
|
9949
|
+
}
|
|
9950
|
+
for (const pattern of ipv6Patterns) {
|
|
9951
|
+
if (pattern.test(ip.toLowerCase())) return true;
|
|
9952
|
+
}
|
|
9953
|
+
return false;
|
|
9954
|
+
}
|
|
9955
|
+
function Proxy$1(options) {
|
|
9956
|
+
const targetUrl = new URL(options.target);
|
|
9957
|
+
if (!["http:", "https:"].includes(targetUrl.protocol)) {
|
|
9958
|
+
throw new Error("Invalid proxy target protocol. Only http and https are allowed.");
|
|
9959
|
+
}
|
|
9960
|
+
if (options.allowedHosts && options.allowedHosts.length > 0) {
|
|
9961
|
+
if (!options.allowedHosts.includes(targetUrl.hostname)) {
|
|
9962
|
+
throw new Error(`Target hostname ${targetUrl.hostname} is not in the allowed hosts list.`);
|
|
9963
|
+
}
|
|
9964
|
+
}
|
|
9965
|
+
if (!options.allowPrivateIPs && isPrivateIP(targetUrl.hostname)) {
|
|
9966
|
+
throw new Error("Proxying to private IP addresses is not allowed.");
|
|
9967
|
+
}
|
|
9968
|
+
return async (ctx, next) => {
|
|
9969
|
+
const req = ctx.request;
|
|
9970
|
+
if (options.ws && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
9971
|
+
const success = ctx.upgrade({
|
|
9972
|
+
data: {
|
|
9973
|
+
handler: {
|
|
9974
|
+
open: (ws) => handleWSOpen(ws, ctx, options, targetUrl),
|
|
9975
|
+
message: (ws, message) => handleWSMessage(ws, message),
|
|
9976
|
+
close: (ws, code, reason) => handleWSClose(ws, code, reason),
|
|
9977
|
+
drain: (ws) => handleWSDrain()
|
|
9978
|
+
}
|
|
9979
|
+
}
|
|
9980
|
+
});
|
|
9981
|
+
if (success) {
|
|
9982
|
+
return void 0;
|
|
9983
|
+
}
|
|
9984
|
+
}
|
|
9985
|
+
let path2 = ctx.url.pathname;
|
|
9986
|
+
if (options.pathRewrite) {
|
|
9987
|
+
path2 = options.pathRewrite(path2);
|
|
9988
|
+
}
|
|
9989
|
+
const url = new URL(path2 + ctx.url.search, targetUrl);
|
|
9990
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
9991
|
+
return ctx.text("Invalid protocol in proxied URL", 400);
|
|
9992
|
+
}
|
|
9993
|
+
const headers = new Headers(req.headers);
|
|
9994
|
+
if (options.changeOrigin) {
|
|
9995
|
+
headers.set("host", targetUrl.host);
|
|
9996
|
+
}
|
|
9997
|
+
if (options.headers) {
|
|
9998
|
+
Object.entries(options.headers).forEach(([key, value]) => headers.set(key, value));
|
|
9999
|
+
}
|
|
10000
|
+
headers.delete("connection");
|
|
10001
|
+
headers.delete("keep-alive");
|
|
10002
|
+
headers.delete("proxy-authenticate");
|
|
10003
|
+
headers.delete("proxy-authorization");
|
|
10004
|
+
headers.delete("te");
|
|
10005
|
+
headers.delete("trailer");
|
|
10006
|
+
headers.delete("transfer-encoding");
|
|
10007
|
+
headers.delete("upgrade");
|
|
10008
|
+
const proxyReq = new Request(url.toString(), {
|
|
10009
|
+
method: req.method,
|
|
10010
|
+
headers,
|
|
10011
|
+
body: req.body,
|
|
10012
|
+
// @ts-ignore - duplex is needed for some node/bun versions for streaming bodies
|
|
10013
|
+
duplex: "half"
|
|
10014
|
+
});
|
|
10015
|
+
const res = await fetch(proxyReq);
|
|
10016
|
+
return new Response(res.body, {
|
|
10017
|
+
status: res.status,
|
|
10018
|
+
statusText: res.statusText,
|
|
10019
|
+
headers: res.headers
|
|
10020
|
+
});
|
|
10021
|
+
};
|
|
10022
|
+
}
|
|
10023
|
+
const wsMap = /* @__PURE__ */ new WeakMap();
|
|
10024
|
+
function handleWSOpen(ws, ctx, options, targetUrl) {
|
|
10025
|
+
let path2 = ctx.url.pathname;
|
|
10026
|
+
if (options.pathRewrite) {
|
|
10027
|
+
path2 = options.pathRewrite(path2);
|
|
10028
|
+
}
|
|
10029
|
+
const url = new URL(path2 + ctx.url.search, targetUrl);
|
|
10030
|
+
url.protocol = targetUrl.protocol.replace("http", "ws");
|
|
10031
|
+
const headers = {};
|
|
10032
|
+
if (options.changeOrigin) {
|
|
10033
|
+
headers["Host"] = targetUrl.host;
|
|
10034
|
+
}
|
|
10035
|
+
ctx.request.headers.forEach((v, k) => {
|
|
10036
|
+
if (!["upgrade", "connection", "sec-websocket-key", "sec-websocket-version", "sec-websocket-extensions"].includes(k.toLowerCase())) {
|
|
10037
|
+
headers[k] = v;
|
|
10038
|
+
}
|
|
10039
|
+
});
|
|
10040
|
+
const upstream = new WebSocket(url.toString());
|
|
10041
|
+
wsMap.set(ws, upstream);
|
|
10042
|
+
const pendingMessages = [];
|
|
10043
|
+
let isConnected = false;
|
|
10044
|
+
upstream.onopen = () => {
|
|
10045
|
+
isConnected = true;
|
|
10046
|
+
while (pendingMessages.length > 0) {
|
|
10047
|
+
const msg = pendingMessages.shift();
|
|
10048
|
+
upstream.send(msg);
|
|
10049
|
+
}
|
|
10050
|
+
};
|
|
10051
|
+
upstream.onmessage = (event) => {
|
|
10052
|
+
ws.send(event.data);
|
|
10053
|
+
};
|
|
10054
|
+
upstream.onclose = (event) => {
|
|
10055
|
+
ws.close(event.code, event.reason);
|
|
10056
|
+
};
|
|
10057
|
+
upstream.onerror = (err) => {
|
|
10058
|
+
console.error("Upstream WebSocket error:", err);
|
|
10059
|
+
ws.close(1011, "Internal Error");
|
|
10060
|
+
};
|
|
10061
|
+
upstream._pendingRequestMessages = pendingMessages;
|
|
10062
|
+
upstream._isConnected = () => isConnected;
|
|
10063
|
+
}
|
|
10064
|
+
function handleWSMessage(ws, message) {
|
|
10065
|
+
const upstream = wsMap.get(ws);
|
|
10066
|
+
if (!upstream) return;
|
|
10067
|
+
if (upstream._isConnected && upstream._isConnected()) {
|
|
10068
|
+
upstream.send(message);
|
|
10069
|
+
} else {
|
|
10070
|
+
upstream._pendingRequestMessages.push(message);
|
|
10071
|
+
}
|
|
10072
|
+
}
|
|
10073
|
+
function handleWSClose(ws, code, reason) {
|
|
10074
|
+
const upstream = wsMap.get(ws);
|
|
10075
|
+
if (upstream) {
|
|
10076
|
+
if (upstream.readyState === WebSocket.OPEN) {
|
|
10077
|
+
upstream.close(code, reason);
|
|
10078
|
+
}
|
|
10079
|
+
wsMap.delete(ws);
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
function handleWSDrain(ws) {
|
|
10083
|
+
}
|
|
8456
10084
|
function SecurityHeaders(options = {}) {
|
|
8457
10085
|
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
8458
10086
|
const set = (k, v) => ctx.response.set(k, v);
|
|
@@ -8665,43 +10293,56 @@ function Session(options) {
|
|
|
8665
10293
|
}
|
|
8666
10294
|
const sessObj = existing;
|
|
8667
10295
|
Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
|
|
8668
|
-
sessObj.save = (
|
|
8669
|
-
|
|
10296
|
+
sessObj.save = () => {
|
|
10297
|
+
return new Promise((resolve, reject) => {
|
|
10298
|
+
store.set(sessObj.id, sessObj, (err) => {
|
|
10299
|
+
if (err) reject(err);
|
|
10300
|
+
else resolve();
|
|
10301
|
+
});
|
|
10302
|
+
});
|
|
8670
10303
|
};
|
|
8671
|
-
sessObj.destroy = (
|
|
8672
|
-
|
|
8673
|
-
|
|
10304
|
+
sessObj.destroy = () => {
|
|
10305
|
+
return new Promise((resolve, reject) => {
|
|
10306
|
+
store.destroy(sessObj.id, (err) => {
|
|
10307
|
+
if (err) reject(err);
|
|
10308
|
+
else resolve();
|
|
10309
|
+
});
|
|
8674
10310
|
});
|
|
8675
10311
|
};
|
|
8676
|
-
sessObj.regenerate = (
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
const
|
|
8682
|
-
|
|
8683
|
-
|
|
10312
|
+
sessObj.regenerate = () => {
|
|
10313
|
+
return new Promise((resolve, reject) => {
|
|
10314
|
+
store.destroy(sessObj.id, (err) => {
|
|
10315
|
+
if (err) return reject(err);
|
|
10316
|
+
sessionID = generateId(ctx);
|
|
10317
|
+
const keys = Object.keys(sessObj);
|
|
10318
|
+
for (let i = 0; i < keys.length; i++) {
|
|
10319
|
+
const key = keys[i];
|
|
10320
|
+
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
10321
|
+
delete sessObj[key];
|
|
10322
|
+
}
|
|
8684
10323
|
}
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
10324
|
+
Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
|
|
10325
|
+
resolve();
|
|
10326
|
+
});
|
|
8688
10327
|
});
|
|
8689
10328
|
};
|
|
8690
10329
|
sessObj.undefined = () => {
|
|
8691
10330
|
};
|
|
8692
|
-
sessObj.reload = (
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
10331
|
+
sessObj.reload = () => {
|
|
10332
|
+
return new Promise((resolve, reject) => {
|
|
10333
|
+
store.get(sessObj.id, (err, sess2) => {
|
|
10334
|
+
if (err) return reject(err);
|
|
10335
|
+
if (!sess2) return reject(new Error("Session not found"));
|
|
10336
|
+
const keys = Object.keys(sessObj);
|
|
10337
|
+
for (let i = 0; i < keys.length; i++) {
|
|
10338
|
+
const key = keys[i];
|
|
10339
|
+
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
10340
|
+
delete sessObj[key];
|
|
10341
|
+
}
|
|
8701
10342
|
}
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
10343
|
+
Object.assign(sessObj, sess2);
|
|
10344
|
+
resolve();
|
|
10345
|
+
});
|
|
8705
10346
|
});
|
|
8706
10347
|
};
|
|
8707
10348
|
sessObj.touch = () => {
|
|
@@ -8793,11 +10434,15 @@ exports.$io = $io;
|
|
|
8793
10434
|
exports.$isApplication = $isApplication;
|
|
8794
10435
|
exports.$isMounted = $isMounted;
|
|
8795
10436
|
exports.$isRouter = $isRouter;
|
|
10437
|
+
exports.$mcpPrompts = $mcpPrompts;
|
|
10438
|
+
exports.$mcpResources = $mcpResources;
|
|
10439
|
+
exports.$mcpTools = $mcpTools;
|
|
8796
10440
|
exports.$middleware = $middleware;
|
|
8797
10441
|
exports.$mountPath = $mountPath;
|
|
8798
10442
|
exports.$parent = $parent;
|
|
8799
10443
|
exports.$rawBody = $rawBody;
|
|
8800
10444
|
exports.$requestId = $requestId;
|
|
10445
|
+
exports.$resilienceConfig = $resilienceConfig;
|
|
8801
10446
|
exports.$routeArgs = $routeArgs;
|
|
8802
10447
|
exports.$routeMatched = $routeMatched;
|
|
8803
10448
|
exports.$routeMethods = $routeMethods;
|
|
@@ -8819,24 +10464,33 @@ exports.Cors = Cors;
|
|
|
8819
10464
|
exports.Ctx = Ctx;
|
|
8820
10465
|
exports.Dashboard = Dashboard;
|
|
8821
10466
|
exports.Delete = Delete;
|
|
10467
|
+
exports.ErrorView = ErrorView;
|
|
8822
10468
|
exports.Event = Event;
|
|
8823
10469
|
exports.Get = Get;
|
|
8824
10470
|
exports.GraphQLApolloPlugin = GraphQLApolloPlugin;
|
|
10471
|
+
exports.GraphQLYogaPlugin = GraphQLYogaPlugin;
|
|
8825
10472
|
exports.HTTPMethods = HTTPMethods;
|
|
8826
10473
|
exports.Head = Head;
|
|
8827
10474
|
exports.Headers = Headers$1;
|
|
10475
|
+
exports.HtmxPlugin = HtmxPlugin;
|
|
10476
|
+
exports.Idempotency = Idempotency;
|
|
8828
10477
|
exports.Inject = Inject;
|
|
8829
10478
|
exports.Injectable = Injectable;
|
|
10479
|
+
exports.MCPServerPlugin = MCPServerPlugin;
|
|
8830
10480
|
exports.MemoryStore = MemoryStore;
|
|
10481
|
+
exports.OpenTelemetryPlugin = OpenTelemetryPlugin;
|
|
8831
10482
|
exports.Options = Options;
|
|
8832
10483
|
exports.Param = Param;
|
|
8833
10484
|
exports.Patch = Patch;
|
|
8834
10485
|
exports.Post = Post;
|
|
10486
|
+
exports.Prompt = Prompt;
|
|
10487
|
+
exports.Proxy = Proxy$1;
|
|
8835
10488
|
exports.Put = Put;
|
|
8836
10489
|
exports.Query = Query;
|
|
8837
10490
|
exports.RateLimit = RateLimit;
|
|
8838
10491
|
exports.RateLimitMiddleware = RateLimitMiddleware;
|
|
8839
10492
|
exports.Req = Req;
|
|
10493
|
+
exports.Resource = Resource;
|
|
8840
10494
|
exports.RouteParamType = RouteParamType;
|
|
8841
10495
|
exports.RouterRegistry = RouterRegistry;
|
|
8842
10496
|
exports.ScalarPlugin = ScalarPlugin;
|
|
@@ -8849,13 +10503,18 @@ exports.ShokupanRequest = ShokupanRequest;
|
|
|
8849
10503
|
exports.ShokupanResponse = ShokupanResponse;
|
|
8850
10504
|
exports.ShokupanRouter = ShokupanRouter;
|
|
8851
10505
|
exports.Spec = Spec;
|
|
10506
|
+
exports.Tool = Tool;
|
|
8852
10507
|
exports.Use = Use;
|
|
8853
10508
|
exports.ValidationError = ValidationError;
|
|
10509
|
+
exports.attachSocketIOBridge = attachSocketIOBridge;
|
|
8854
10510
|
exports.compileValidators = compileValidators;
|
|
8855
10511
|
exports.compose = compose;
|
|
8856
10512
|
exports.enableOpenApiValidation = enableOpenApiValidation;
|
|
8857
10513
|
exports.openApiValidator = openApiValidator;
|
|
8858
10514
|
exports.precompileValidators = precompileValidators;
|
|
10515
|
+
exports.serveStatic = serveStatic;
|
|
10516
|
+
exports.traceHandler = traceHandler;
|
|
10517
|
+
exports.traceMiddleware = traceMiddleware;
|
|
8859
10518
|
exports.useExpress = useExpress;
|
|
8860
10519
|
exports.valibot = valibot;
|
|
8861
10520
|
exports.validate = validate;
|