shokupan 0.9.0 → 0.10.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.
Files changed (49) hide show
  1. package/dist/analyzer-BqIe1p0R.js +35 -0
  2. package/dist/analyzer-BqIe1p0R.js.map +1 -0
  3. package/dist/analyzer-CKLGLFtx.cjs +35 -0
  4. package/dist/analyzer-CKLGLFtx.cjs.map +1 -0
  5. package/dist/{analyzer-Ce_7JxZh.js → analyzer.impl-CV6W1Eq7.js} +238 -21
  6. package/dist/analyzer.impl-CV6W1Eq7.js.map +1 -0
  7. package/dist/{analyzer-Bei1sVWp.cjs → analyzer.impl-D9Yi1Hax.cjs} +237 -20
  8. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +1 -0
  9. package/dist/cli.cjs +1 -1
  10. package/dist/cli.js +1 -1
  11. package/dist/context.d.ts +19 -7
  12. package/dist/http-server-BEMPIs33.cjs.map +1 -1
  13. package/dist/http-server-CCeagTyU.js.map +1 -1
  14. package/dist/index.cjs +1500 -275
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.js +1482 -256
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/application/api-explorer/plugin.d.ts +9 -0
  20. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +880 -0
  21. package/dist/plugins/application/api-explorer/static/style.css +767 -0
  22. package/dist/plugins/application/api-explorer/static/theme.css +128 -0
  23. package/dist/plugins/application/asyncapi/generator.d.ts +3 -0
  24. package/dist/plugins/application/asyncapi/plugin.d.ts +15 -0
  25. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +748 -0
  26. package/dist/plugins/application/asyncapi/static/style.css +565 -0
  27. package/dist/plugins/application/asyncapi/static/theme.css +128 -0
  28. package/dist/plugins/application/auth.d.ts +3 -1
  29. package/dist/plugins/application/dashboard/metrics-collector.d.ts +3 -1
  30. package/dist/plugins/application/dashboard/plugin.d.ts +13 -3
  31. package/dist/plugins/application/dashboard/static/registry.css +0 -53
  32. package/dist/plugins/application/dashboard/static/styles.css +29 -20
  33. package/dist/plugins/application/dashboard/static/tabulator.css +83 -31
  34. package/dist/plugins/application/dashboard/static/theme.css +128 -0
  35. package/dist/plugins/application/graphql-apollo.d.ts +33 -0
  36. package/dist/plugins/application/graphql-yoga.d.ts +25 -0
  37. package/dist/plugins/application/openapi/analyzer.d.ts +12 -119
  38. package/dist/plugins/application/openapi/analyzer.impl.d.ts +167 -0
  39. package/dist/plugins/application/scalar.d.ts +9 -2
  40. package/dist/router.d.ts +80 -51
  41. package/dist/shokupan.d.ts +14 -8
  42. package/dist/util/datastore.d.ts +71 -7
  43. package/dist/util/decorators.d.ts +2 -2
  44. package/dist/util/types.d.ts +96 -3
  45. package/package.json +32 -12
  46. package/dist/analyzer-Bei1sVWp.cjs.map +0 -1
  47. package/dist/analyzer-Ce_7JxZh.js.map +0 -1
  48. package/dist/plugins/application/dashboard/static/scrollbar.css +0 -24
  49. package/dist/plugins/application/dashboard/template.eta +0 -246
package/dist/index.js CHANGED
@@ -1,21 +1,24 @@
1
1
  import { nanoid } from "nanoid";
2
2
  import { readFile } from "node:fs/promises";
3
- import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
4
- import { AsyncLocalStorage } from "node:async_hooks";
5
- import { Surreal, RecordId } from "surrealdb";
3
+ import { inspect } from "node:util";
4
+ import { RecordId, Surreal } from "surrealdb";
6
5
  import { Eta } from "eta";
7
6
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
8
7
  import { resolve, join, sep, basename } from "path";
8
+ import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
9
+ import { dump } from "js-yaml";
10
+ import { AsyncLocalStorage } from "node:async_hooks";
9
11
  import * as os from "node:os";
10
12
  import os__default from "node:os";
11
- import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
12
- import * as jose from "jose";
13
+ import { dirname, join as join$1 } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import renderToString from "preact-render-to-string";
16
+ import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
13
17
  import cluster from "node:cluster";
14
18
  import net from "node:net";
15
- import { dirname } from "node:path";
16
- import { fileURLToPath } from "node:url";
17
19
  import { monitorEventLoopDelay } from "node:perf_hooks";
18
- import { OpenAPIAnalyzer } from "./analyzer-Ce_7JxZh.js";
20
+ import { readFileSync } from "node:fs";
21
+ import { OpenAPIAnalyzer } from "./analyzer-BqIe1p0R.js";
19
22
  import * as zlib from "node:zlib";
20
23
  import Ajv from "ajv";
21
24
  import addFormats from "ajv-formats";
@@ -268,6 +271,21 @@ class ShokupanContext {
268
271
  [$cachedHost];
269
272
  [$cachedOrigin];
270
273
  [$cachedQuery];
274
+ disconnectCallbacks = [];
275
+ /**
276
+ * Registers a callback to be executed when the associated WebSocket disconnects.
277
+ * This is only applicable for requests that are part of a WebSocket interaction or upgrade.
278
+ */
279
+ onSocketDisconnect(callback) {
280
+ this.disconnectCallbacks.push(callback);
281
+ }
282
+ /**
283
+ * @internal
284
+ * Retrieves registered disconnect callbacks for execution.
285
+ */
286
+ getDisconnectCallbacks() {
287
+ return this.disconnectCallbacks;
288
+ }
271
289
  [$ws];
272
290
  [$socket];
273
291
  [$io];
@@ -282,6 +300,20 @@ class ShokupanContext {
282
300
  get requestId() {
283
301
  return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid();
284
302
  }
303
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
304
+ const innerString = inspect({
305
+ method: this.request.method,
306
+ url: this.request.url,
307
+ requestHeaders: new Map(this.request.headers),
308
+ sessionId: this.sessionID,
309
+ state: this.state,
310
+ params: this.params,
311
+ response: this[$finalResponse]?.body,
312
+ responseHeaders: new Map(this[$finalResponse]?.headers),
313
+ handlerStack: this.handlerStack.map((h) => h.name === "anonymous" ? h.file + ":" + h.line : h.name)
314
+ }, { depth: null, colors: true, numericSeparator: true, customInspect: true });
315
+ return "Context(" + this.requestId + ") {" + innerString.slice(1, -2) + ",\n ...others\n}";
316
+ }
285
317
  get url() {
286
318
  if (!this[$url]) {
287
319
  const urlString = this.request.url || "http://localhost/";
@@ -333,10 +365,8 @@ class ShokupanContext {
333
365
  if (this[$cachedQuery]) return this[$cachedQuery];
334
366
  const q = /* @__PURE__ */ Object.create(null);
335
367
  const blocklist = ["__proto__", "constructor", "prototype"];
336
- const entries = Object.entries(this.url.searchParams);
337
- for (let i = 0; i < entries.length; i++) {
338
- const [key, value] = entries[i];
339
- if (blocklist.includes(key)) continue;
368
+ this.url.searchParams.forEach((value, key) => {
369
+ if (blocklist.includes(key)) return;
340
370
  if (Object.prototype.hasOwnProperty.call(q, key)) {
341
371
  if (Array.isArray(q[key])) {
342
372
  q[key].push(value);
@@ -346,7 +376,7 @@ class ShokupanContext {
346
376
  } else {
347
377
  q[key] = value;
348
378
  }
349
- }
379
+ });
350
380
  this[$cachedQuery] = q;
351
381
  return q;
352
382
  }
@@ -639,12 +669,12 @@ class ShokupanContext {
639
669
  /**
640
670
  * Respond with a JSON object
641
671
  */
642
- json(data, status, headers) {
672
+ async json(data, status, headers) {
643
673
  const finalStatus = status ?? this.response.status ?? 200;
644
674
  if (!VALID_HTTP_STATUSES.has(finalStatus)) {
645
675
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
646
676
  }
647
- const jsonString = JSON.stringify(data);
677
+ const jsonString = JSON.stringify(data instanceof Promise ? await data : data);
648
678
  this[$rawBody] = jsonString;
649
679
  if (!headers && !this.response.hasPopulatedHeaders) {
650
680
  this[$finalResponse] = new Response(jsonString, {
@@ -661,14 +691,14 @@ class ShokupanContext {
661
691
  /**
662
692
  * Respond with a text string
663
693
  */
664
- text(data, status, headers) {
694
+ async text(data, status, headers) {
665
695
  const finalStatus = status ?? this.response.status ?? 200;
666
696
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
667
697
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
668
698
  }
669
- this[$rawBody] = data;
699
+ this[$rawBody] = data instanceof Promise ? await data : data;
670
700
  if (!headers && !this.response.hasPopulatedHeaders) {
671
- this[$finalResponse] = new Response(data, {
701
+ this[$finalResponse] = new Response(this[$rawBody], {
672
702
  status: finalStatus,
673
703
  headers: { "content-type": "text/plain; charset=utf-8" }
674
704
  });
@@ -676,71 +706,73 @@ class ShokupanContext {
676
706
  }
677
707
  const finalHeaders = this.mergeHeaders(headers);
678
708
  finalHeaders.set("content-type", "text/plain; charset=utf-8");
679
- this[$finalResponse] = new Response(data, { status: finalStatus, headers: finalHeaders });
709
+ this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
680
710
  return this[$finalResponse];
681
711
  }
682
712
  /**
683
713
  * Respond with HTML content
684
714
  */
685
- html(html, status, headers) {
715
+ async html(html, status, headers) {
686
716
  const finalStatus = status ?? this.response.status ?? 200;
687
717
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
688
718
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
689
719
  }
690
720
  const finalHeaders = this.mergeHeaders(headers);
691
721
  finalHeaders.set("content-type", "text/html; charset=utf-8");
692
- this[$rawBody] = html;
693
- this[$finalResponse] = new Response(html, { status: finalStatus, headers: finalHeaders });
722
+ this[$rawBody] = html instanceof Promise ? await html : html;
723
+ this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
694
724
  return this[$finalResponse];
695
725
  }
696
726
  /**
697
727
  * Respond with a redirect
698
728
  */
699
- redirect(url, status = 302) {
729
+ async redirect(url, status = 302) {
700
730
  if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
701
731
  throw new Error(`Invalid redirect status code: ${status}`);
702
732
  }
703
- const headers = this.mergeHeaders();
704
- headers.set("Location", url);
705
- this[$finalResponse] = new Response(null, { status, headers });
733
+ const finalHeaders = this.mergeHeaders();
734
+ finalHeaders.set("Location", url instanceof Promise ? await url : url);
735
+ this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
706
736
  return this[$finalResponse];
707
737
  }
708
738
  /**
709
739
  * Respond with a status code
710
740
  * DOES NOT CHAIN!
711
741
  */
712
- status(status) {
742
+ async status(statusCode) {
743
+ const status = statusCode instanceof Promise ? await statusCode : statusCode;
713
744
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
714
745
  throw new Error(`Invalid HTTP status code: ${status}`);
715
746
  }
716
- const headers = this.mergeHeaders();
717
- this[$finalResponse] = new Response(null, { status, headers });
747
+ const finalHeaders = this.mergeHeaders();
748
+ this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
718
749
  return this[$finalResponse];
719
750
  }
720
751
  /**
721
752
  * Respond with a file
722
753
  */
723
754
  async file(path, fileOptions, responseOptions) {
724
- const headers = this.mergeHeaders(responseOptions?.headers);
755
+ const finalHeaders = this.mergeHeaders(responseOptions?.headers);
725
756
  const status = responseOptions?.status ?? this.response.status;
726
757
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
727
758
  throw new Error(`Invalid HTTP status code: ${status}`);
728
759
  }
729
760
  if (typeof Bun !== "undefined") {
730
- this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers });
761
+ this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers: finalHeaders });
731
762
  return this[$finalResponse];
732
763
  } else {
733
764
  const fileBuffer = await readFile(path);
734
765
  if (fileOptions?.type) {
735
- headers.set("content-type", fileOptions.type);
766
+ finalHeaders.set("content-type", fileOptions.type);
736
767
  }
737
- this[$finalResponse] = new Response(fileBuffer, { status, headers });
768
+ this[$finalResponse] = new Response(fileBuffer, { status, headers: finalHeaders });
738
769
  return this[$finalResponse];
739
770
  }
740
771
  }
741
772
  /**
742
773
  * Render a JSX element
743
774
  * @param element JSX Element
775
+ * @param args JSX Element Args/Props
744
776
  * @param status HTTP Status
745
777
  * @param headers HTTP Headers
746
778
  */
@@ -794,29 +826,6 @@ const compose = (middleware) => {
794
826
  return runner(0);
795
827
  };
796
828
  };
797
- const tracer = trace.getTracer("shokupan.middleware");
798
- function traceHandler(fn, name) {
799
- return async function(...args) {
800
- return tracer.startActiveSpan(`route handler - ${name}`, {
801
- kind: SpanKind.INTERNAL,
802
- attributes: {
803
- "http.route": name,
804
- "component": "shokupan.route"
805
- }
806
- }, async (span) => {
807
- try {
808
- const result = await fn.apply(this, args);
809
- return result;
810
- } catch (err) {
811
- span.recordException(err);
812
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
813
- throw err;
814
- } finally {
815
- span.end();
816
- }
817
- });
818
- };
819
- }
820
829
  function isObject(item) {
821
830
  return item && typeof item === "object" && !Array.isArray(item);
822
831
  }
@@ -989,24 +998,34 @@ function analyzeHandler(handler) {
989
998
  }
990
999
  return { inferredSpec };
991
1000
  }
992
- async function getAstRoutes(applications) {
1001
+ async function getAstRoutes$1(applications) {
993
1002
  const astRoutes = [];
994
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
1003
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
995
1004
  if (seen.has(app.name)) return [];
996
1005
  const newSeen = new Set(seen);
997
1006
  newSeen.add(app.name);
998
1007
  const expanded = [];
1008
+ let currentPrefix = prefix;
1009
+ if (app.controllerPrefix) {
1010
+ const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
1011
+ const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
1012
+ currentPrefix = cleanPrefix + cleanCont;
1013
+ }
999
1014
  for (const route of app.routes) {
1000
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1015
+ const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
1001
1016
  const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1002
1017
  let joined = cleanPrefix + cleanPath;
1003
1018
  if (joined.length > 1 && joined.endsWith("/")) {
1004
1019
  joined = joined.slice(0, -1);
1005
1020
  }
1006
- expanded.push({
1021
+ const expandedRoute = {
1007
1022
  ...route,
1008
1023
  path: joined || "/"
1009
- });
1024
+ };
1025
+ if (sourceOverride) {
1026
+ expandedRoute.sourceContext = sourceOverride;
1027
+ }
1028
+ expanded.push(expandedRoute);
1010
1029
  }
1011
1030
  if (app.mounted) {
1012
1031
  for (const mount of app.mounted) {
@@ -1014,7 +1033,23 @@ async function getAstRoutes(applications) {
1014
1033
  if (targetApp) {
1015
1034
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1016
1035
  const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
1017
- expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
1036
+ let nextSourceOverride = sourceOverride;
1037
+ if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
1038
+ if (mount.sourceContext) {
1039
+ nextSourceOverride = {
1040
+ ...mount.sourceContext,
1041
+ // Add highlight for the mount line to make it clear
1042
+ highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
1043
+ highlights: [{
1044
+ startLine: mount.sourceContext.startLine,
1045
+ endLine: mount.sourceContext.endLine,
1046
+ type: "return-success"
1047
+ // Use the success color (cyan) for the mount point
1048
+ }]
1049
+ };
1050
+ }
1051
+ }
1052
+ expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
1018
1053
  }
1019
1054
  }
1020
1055
  }
@@ -1042,13 +1077,13 @@ async function generateOpenApi(rootRouter, options = {}) {
1042
1077
  const defaultTagName = options.defaultTag || "Application";
1043
1078
  let astRoutes = [];
1044
1079
  try {
1045
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-Ce_7JxZh.js");
1080
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
1046
1081
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
1047
1082
  const { applications } = await analyzer.analyze();
1048
- astRoutes = await getAstRoutes(applications);
1083
+ astRoutes = await getAstRoutes$1(applications);
1049
1084
  } catch (e) {
1050
1085
  }
1051
- const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
1086
+ const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = []) => {
1052
1087
  let group = currentGroup;
1053
1088
  let tag = defaultTag;
1054
1089
  if (router.config?.group) group = router.config.group;
@@ -1065,21 +1100,33 @@ async function generateOpenApi(rootRouter, options = {}) {
1065
1100
  }
1066
1101
  }
1067
1102
  if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
1103
+ const routerMiddleware = router.middleware || [];
1068
1104
  const routes = router[$routes] || [];
1069
1105
  for (const route of routes) {
1106
+ if (!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"].includes(route.method.toUpperCase())) {
1107
+ continue;
1108
+ }
1070
1109
  const routeGroup = route.group || group;
1071
1110
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1072
1111
  const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1073
1112
  let fullPath = cleanPrefix + cleanSubPath || "/";
1074
- fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
1075
1113
  if (fullPath.length > 1 && fullPath.endsWith("/")) {
1076
1114
  fullPath = fullPath.slice(0, -1);
1077
1115
  }
1116
+ fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
1078
1117
  if (!paths[fullPath]) paths[fullPath] = {};
1079
1118
  const operation = {
1080
1119
  responses: { "200": { description: "Successful response" } },
1081
1120
  tags: [tag]
1082
1121
  };
1122
+ const routeMiddleware = route.middleware || [];
1123
+ const allMiddleware = [...inheritedMiddleware, ...routerMiddleware, ...routeMiddleware];
1124
+ if (allMiddleware.length > 0) {
1125
+ operation["x-shokupan-middleware"] = allMiddleware.map((mw) => ({
1126
+ name: mw.name || "middleware",
1127
+ metadata: mw.metadata
1128
+ }));
1129
+ }
1083
1130
  if (route.guards) {
1084
1131
  for (const guard of route.guards) {
1085
1132
  if (guard.spec) {
@@ -1117,6 +1164,23 @@ async function generateOpenApi(rootRouter, options = {}) {
1117
1164
  if (astMatch.description) operation.description = astMatch.description;
1118
1165
  if (astMatch.tags) operation.tags = astMatch.tags;
1119
1166
  if (astMatch.operationId) operation.operationId = astMatch.operationId;
1167
+ if (astMatch.sourceContext) {
1168
+ const sc = astMatch.sourceContext;
1169
+ operation["x-source-info"] = {
1170
+ file: sc.file,
1171
+ line: sc.startLine,
1172
+ snippet: sc.snippet || astMatch.handlerSource,
1173
+ // Fallback
1174
+ offset: sc.snippetStartLine || sc.startLine,
1175
+ highlightLines: [sc.startLine, sc.endLine],
1176
+ highlights: sc.highlights
1177
+ };
1178
+ operation["x-shokupan-source"] = {
1179
+ file: sc.file,
1180
+ line: sc.startLine,
1181
+ code: sc.snippet || astMatch.handlerSource || ""
1182
+ };
1183
+ }
1120
1184
  if (astMatch.requestTypes?.body) {
1121
1185
  operation.requestBody = {
1122
1186
  content: { "application/json": { schema: astMatch.requestTypes.body } }
@@ -1128,10 +1192,12 @@ async function generateOpenApi(rootRouter, options = {}) {
1128
1192
  content: { "application/json": { schema: astMatch.responseSchema } }
1129
1193
  };
1130
1194
  } else if (astMatch.responseType) {
1131
- const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
1195
+ let contentType = "application/json";
1196
+ if (astMatch.responseType === "string") contentType = "text/plain";
1197
+ else if (astMatch.responseType === "html") contentType = "text/html";
1132
1198
  operation.responses["200"] = {
1133
1199
  description: "Successful response",
1134
- content: { [contentType]: { schema: { type: astMatch.responseType } } }
1200
+ content: { [contentType]: { schema: { type: "string" } } }
1135
1201
  };
1136
1202
  }
1137
1203
  const params = [];
@@ -1143,6 +1209,26 @@ async function generateOpenApi(rootRouter, options = {}) {
1143
1209
  if (params.length > 0) {
1144
1210
  operation.parameters = params;
1145
1211
  }
1212
+ } else {
1213
+ const runtimeSource = (route.handler.originalHandler || route.handler).toString();
1214
+ let file;
1215
+ let line;
1216
+ if (route.metadata?.file) {
1217
+ file = route.metadata.file;
1218
+ line = route.metadata.line || 1;
1219
+ }
1220
+ operation["x-source-info"] = {
1221
+ snippet: runtimeSource,
1222
+ isRuntime: true,
1223
+ ...file ? { file, line: line || 1 } : {}
1224
+ };
1225
+ if (file) {
1226
+ operation["x-shokupan-source"] = {
1227
+ file,
1228
+ line: line || 1,
1229
+ code: runtimeSource
1230
+ };
1231
+ }
1146
1232
  }
1147
1233
  if (route.keys.length > 0) {
1148
1234
  const pathParams = route.keys.map((key) => ({
@@ -1212,7 +1298,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1212
1298
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1213
1299
  const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
1214
1300
  const nextPrefix = cleanPrefix + cleanMount || "/";
1215
- collect(child, nextPrefix, group, tag);
1301
+ collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware]);
1216
1302
  }
1217
1303
  };
1218
1304
  collect(rootRouter);
@@ -1231,41 +1317,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1231
1317
  "x-tagGroups": xTagGroups
1232
1318
  };
1233
1319
  }
1234
- class RequestContextStore {
1235
- request;
1236
- span;
1237
- }
1238
- const asyncContext = new AsyncLocalStorage();
1239
- class HttpError extends Error {
1240
- status;
1241
- constructor(message, status) {
1242
- super(message);
1243
- this.name = "HttpError";
1244
- this.status = status;
1245
- if (Error.captureStackTrace) {
1246
- Error.captureStackTrace(this, HttpError);
1247
- }
1248
- }
1249
- }
1250
- function getErrorStatus(err) {
1251
- if (!err || typeof err !== "object") {
1252
- return 500;
1253
- }
1254
- if (typeof err.status === "number") {
1255
- return err.status;
1256
- }
1257
- if (typeof err.statusCode === "number") {
1258
- return err.statusCode;
1259
- }
1260
- return 500;
1261
- }
1262
- class EventError extends HttpError {
1263
- constructor(message = "Event Error") {
1264
- super(message, 500);
1265
- this.name = "EventError";
1266
- }
1267
- }
1268
- const eta$1 = new Eta();
1320
+ const eta = new Eta();
1269
1321
  function serveStatic(config, prefix) {
1270
1322
  const rootPath = resolve(config.root || ".");
1271
1323
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
@@ -1364,7 +1416,7 @@ function serveStatic(config, prefix) {
1364
1416
  if (config.listDirectory) {
1365
1417
  try {
1366
1418
  const files = await readdir(requestPath);
1367
- const listing = eta$1.renderString(`
1419
+ const listing = eta.renderString(`
1368
1420
  <!DOCTYPE html>
1369
1421
  <html>
1370
1422
  <head>
@@ -1404,7 +1456,7 @@ function serveStatic(config, prefix) {
1404
1456
  if (typeof Bun !== "undefined") {
1405
1457
  response = new Response(Bun.file(finalPath));
1406
1458
  } else {
1407
- const fileBuffer = await readFile$1(finalPath);
1459
+ const fileBuffer = await readFile$1(finalPath, { encoding: "binary" });
1408
1460
  response = new Response(fileBuffer);
1409
1461
  }
1410
1462
  if (config.hooks?.onResponse) {
@@ -1417,67 +1469,6 @@ function serveStatic(config, prefix) {
1417
1469
  serveStaticMiddleware.pluginName = "ServeStatic";
1418
1470
  return serveStaticMiddleware;
1419
1471
  }
1420
- const G = globalThis;
1421
- G.__shokupan_db = G.__shokupan_db || null;
1422
- G.__shokupan_db_promise = G.__shokupan_db_promise || null;
1423
- async function ensureDb() {
1424
- if (G.__shokupan_db) return G.__shokupan_db;
1425
- if (G.__shokupan_db_promise) return G.__shokupan_db_promise;
1426
- G.__shokupan_db_promise = (async () => {
1427
- try {
1428
- const { createNodeEngines } = await import("@surrealdb/node");
1429
- const surreal = await import("surrealdb");
1430
- const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1431
- const _db = new Surreal({
1432
- engines: createNodeEngines()
1433
- });
1434
- await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1435
- await _db.query(`
1436
- DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1437
- DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1438
- DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1439
- DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1440
- DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1441
- DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1442
- DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
1443
- `);
1444
- G.__shokupan_db = _db;
1445
- return _db;
1446
- } catch (e) {
1447
- G.__shokupan_db_promise = null;
1448
- if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1449
- throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1450
- }
1451
- throw e;
1452
- }
1453
- })();
1454
- return G.__shokupan_db_promise;
1455
- }
1456
- const datastore = {
1457
- async get(recordId) {
1458
- await ensureDb();
1459
- return G.__shokupan_db.select(recordId);
1460
- },
1461
- async set(recordId, value) {
1462
- await ensureDb();
1463
- return G.__shokupan_db.upsert(recordId).content(value);
1464
- },
1465
- async query(query, vars) {
1466
- await ensureDb();
1467
- try {
1468
- return G.__shokupan_db.query(query, vars).collect();
1469
- } catch (e) {
1470
- console.error("DS ERROR:", e);
1471
- throw e;
1472
- }
1473
- },
1474
- get ready() {
1475
- return ensureDb().then(() => void 0);
1476
- }
1477
- };
1478
- process.on("exit", async () => {
1479
- if (G.__shokupan_db) await G.__shokupan_db.close();
1480
- });
1481
1472
  class Container {
1482
1473
  static services = /* @__PURE__ */ new Map();
1483
1474
  static register(target, instance) {
@@ -1511,6 +1502,58 @@ function Inject(token) {
1511
1502
  });
1512
1503
  };
1513
1504
  }
1505
+ class HttpError extends Error {
1506
+ status;
1507
+ constructor(message, status) {
1508
+ super(message);
1509
+ this.name = "HttpError";
1510
+ this.status = status;
1511
+ if (Error.captureStackTrace) {
1512
+ Error.captureStackTrace(this, HttpError);
1513
+ }
1514
+ }
1515
+ }
1516
+ function getErrorStatus(err) {
1517
+ if (!err || typeof err !== "object") {
1518
+ return 500;
1519
+ }
1520
+ if (typeof err.status === "number") {
1521
+ return err.status;
1522
+ }
1523
+ if (typeof err.statusCode === "number") {
1524
+ return err.statusCode;
1525
+ }
1526
+ return 500;
1527
+ }
1528
+ class EventError extends HttpError {
1529
+ constructor(message = "Event Error") {
1530
+ super(message, 500);
1531
+ this.name = "EventError";
1532
+ }
1533
+ }
1534
+ const tracer = trace.getTracer("shokupan.middleware");
1535
+ function traceHandler(fn, name) {
1536
+ return async function(...args) {
1537
+ return tracer.startActiveSpan(`route handler - ${name}`, {
1538
+ kind: SpanKind.INTERNAL,
1539
+ attributes: {
1540
+ "http.route": name,
1541
+ "component": "shokupan.route"
1542
+ }
1543
+ }, async (span) => {
1544
+ try {
1545
+ const result = await fn.apply(this, args);
1546
+ return result;
1547
+ } catch (err) {
1548
+ span.recordException(err);
1549
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
1550
+ throw err;
1551
+ } finally {
1552
+ span.end();
1553
+ }
1554
+ });
1555
+ };
1556
+ }
1514
1557
  class ShokupanRequestBase {
1515
1558
  method;
1516
1559
  url;
@@ -1548,8 +1591,10 @@ function getCallerInfo(skipFrames = 1) {
1548
1591
  if (!l.includes(":")) continue;
1549
1592
  if (l.includes("node_modules")) continue;
1550
1593
  if (l.includes("bun:main")) continue;
1594
+ if (l.includes("bun:wrap")) continue;
1551
1595
  if (l.includes("src/util/stack.ts")) continue;
1552
1596
  if (l.includes("src/router.ts")) continue;
1597
+ if (l.includes("src/util/decorators.ts")) continue;
1553
1598
  if (l.includes("src/shokupan.ts")) continue;
1554
1599
  found++;
1555
1600
  if (found >= skipFrames) {
@@ -1675,6 +1720,8 @@ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1675
1720
  RouteParamType2["CONTEXT"] = "CONTEXT";
1676
1721
  return RouteParamType2;
1677
1722
  })(RouteParamType || {});
1723
+ const RouterRegistry = /* @__PURE__ */ new Map();
1724
+ const ShokupanApplicationTree = {};
1678
1725
  class ShokupanRouter {
1679
1726
  constructor(config) {
1680
1727
  this.config = config;
@@ -1692,6 +1739,9 @@ class ShokupanRouter {
1692
1739
  [$parent] = null;
1693
1740
  [$childRouters] = [];
1694
1741
  [$childControllers] = [];
1742
+ get db() {
1743
+ return this.root?.db;
1744
+ }
1695
1745
  hookCache = /* @__PURE__ */ new Map();
1696
1746
  hooksInitialized = false;
1697
1747
  middleware = [];
@@ -1708,6 +1758,14 @@ class ShokupanRouter {
1708
1758
  // Metadata for the router itself
1709
1759
  currentGuards = [];
1710
1760
  eventHandlers = /* @__PURE__ */ new Map();
1761
+ /**
1762
+ * Registers middleware for this router.
1763
+ * Middleware will run for all routes matched by this router.
1764
+ */
1765
+ use(middleware) {
1766
+ this.middleware.push(middleware);
1767
+ return this;
1768
+ }
1711
1769
  // Registry Accessor
1712
1770
  getComponentRegistry() {
1713
1771
  const controllerRoutesMap = /* @__PURE__ */ new Map();
@@ -1772,6 +1830,8 @@ class ShokupanRouter {
1772
1830
  * Registers an event handler for WebSocket.
1773
1831
  */
1774
1832
  event(name, handler) {
1833
+ const info = getCallerInfo();
1834
+ handler.source = { file: info.file, line: info.line };
1775
1835
  if (this.eventHandlers.has(name)) {
1776
1836
  const err = new EventError(`Event handler \`${name}\` already exists.`);
1777
1837
  console.warn(err);
@@ -1796,6 +1856,12 @@ class ShokupanRouter {
1796
1856
  }
1797
1857
  return null;
1798
1858
  }
1859
+ /**
1860
+ * Returns all registered event handlers.
1861
+ */
1862
+ getEventHandlers() {
1863
+ return this.eventHandlers;
1864
+ }
1799
1865
  /**
1800
1866
  * Mounts a controller instance to a path prefix.
1801
1867
  *
@@ -2039,10 +2105,12 @@ class ShokupanRouter {
2039
2105
  if (typeof originalHandler !== "function") continue;
2040
2106
  let method;
2041
2107
  let subPath = "";
2108
+ let methodSource;
2042
2109
  if (decoratedRoutes && decoratedRoutes.has(name)) {
2043
2110
  const config = decoratedRoutes.get(name);
2044
2111
  method = config.method;
2045
2112
  subPath = config.path;
2113
+ methodSource = config.source;
2046
2114
  } else {
2047
2115
  for (let j = 0; j < HTTPMethods.length; j++) {
2048
2116
  const m = HTTPMethods[j];
@@ -2172,7 +2240,16 @@ class ShokupanRouter {
2172
2240
  const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2173
2241
  const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2174
2242
  const spec = { tags: [tagName], ...userSpec };
2175
- this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2243
+ this.add({
2244
+ method,
2245
+ path: normalizedPath,
2246
+ handler: finalHandler,
2247
+ spec,
2248
+ controller: instance,
2249
+ metadata: methodSource || instance.metadata,
2250
+ middleware: allMiddleware
2251
+ // Capture all resolved middleware
2252
+ });
2176
2253
  }
2177
2254
  if (decoratedEvents?.has(name)) {
2178
2255
  routesAttached++;
@@ -2205,6 +2282,11 @@ class ShokupanRouter {
2205
2282
  }
2206
2283
  return originalHandler.apply(instance, args);
2207
2284
  };
2285
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2286
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2287
+ const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
2288
+ wrappedHandler.spec = spec;
2289
+ wrappedHandler.originalHandler = originalHandler;
2208
2290
  this.event(config.eventName, wrappedHandler);
2209
2291
  }
2210
2292
  }
@@ -2274,7 +2356,7 @@ class ShokupanRouter {
2274
2356
  * @param arg.renderer - JSX renderer for the route
2275
2357
  * @param arg.controller - Controller for the route
2276
2358
  */
2277
- add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
2359
+ add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller, metadata, middleware }) {
2278
2360
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
2279
2361
  if (this.currentGuards.length > 0) {
2280
2362
  spec = spec || {};
@@ -2292,7 +2374,13 @@ class ShokupanRouter {
2292
2374
  }
2293
2375
  }
2294
2376
  }
2295
- let wrappedHandler = handler;
2377
+ let wrappedHandler = async (ctx) => {
2378
+ if (ctx.upgrade()) {
2379
+ return void 0;
2380
+ }
2381
+ return handler(ctx);
2382
+ };
2383
+ wrappedHandler.originalHandler = handler.originalHandler || handler;
2296
2384
  const routeGuards = [...this.currentGuards];
2297
2385
  const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
2298
2386
  if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
@@ -2343,7 +2431,7 @@ class ShokupanRouter {
2343
2431
  return innerHandler(ctx);
2344
2432
  };
2345
2433
  }
2346
- const { file, line } = getCallerInfo();
2434
+ const { file, line } = metadata || getCallerInfo();
2347
2435
  const trackingHandler = wrappedHandler;
2348
2436
  wrappedHandler = async (ctx) => {
2349
2437
  if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
@@ -2369,8 +2457,10 @@ class ShokupanRouter {
2369
2457
  const config = ctx.app.applicationConfig;
2370
2458
  Promise.resolve().then(async () => {
2371
2459
  try {
2460
+ const db = ctx.app?.db;
2461
+ if (!db) return;
2372
2462
  const timestamp = Date.now();
2373
- await datastore.set(new RecordId("middleware_tracking", {
2463
+ await db.upsert(new RecordId("middleware_tracking", {
2374
2464
  timestamp,
2375
2465
  name: handler.name || "anonymous"
2376
2466
  }), {
@@ -2389,11 +2479,11 @@ class ShokupanRouter {
2389
2479
  const ttl = config.middlewareTrackingTTL ?? 864e5;
2390
2480
  const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2391
2481
  const cutoff = Date.now() - ttl;
2392
- await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2393
- const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2482
+ await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2483
+ const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
2394
2484
  if (results?.[0]?.count > maxCapacity) {
2395
2485
  const toDelete = results[0].count - maxCapacity;
2396
- await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2486
+ await db.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2397
2487
  }
2398
2488
  } catch (datastoreError) {
2399
2489
  console.error("Failed to store middleware tracking:", datastoreError);
@@ -2423,7 +2513,8 @@ class ShokupanRouter {
2423
2513
  file,
2424
2514
  line
2425
2515
  },
2426
- controller
2516
+ controller,
2517
+ middleware: middleware || []
2427
2518
  });
2428
2519
  this.trie.insert(method, path, bakedHandler);
2429
2520
  return this;
@@ -2552,7 +2643,8 @@ class ShokupanRouter {
2552
2643
  method,
2553
2644
  path,
2554
2645
  spec,
2555
- handler: finalHandler
2646
+ handler: finalHandler,
2647
+ middleware: handlers.slice(0, handlers.length - 1)
2556
2648
  });
2557
2649
  }
2558
2650
  /**
@@ -2592,7 +2684,7 @@ class ShokupanRouter {
2592
2684
  }
2593
2685
  this.hooksInitialized = true;
2594
2686
  }
2595
- async runHooks(name, ...args) {
2687
+ runHooks(name, ...args) {
2596
2688
  if (!this.hooksInitialized) {
2597
2689
  this.ensureHooksInitialized();
2598
2690
  }
@@ -2601,7 +2693,7 @@ class ShokupanRouter {
2601
2693
  const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2602
2694
  const debug = ctx?.[$debug];
2603
2695
  if (debug) {
2604
- await Promise.all(fns.map(async (fn, index) => {
2696
+ return Promise.all(fns.map(async (fn, index) => {
2605
2697
  const hookId = `hook_${name}_${fn.name || index}`;
2606
2698
  const previousNode = debug.getCurrentNode();
2607
2699
  debug.trackEdge(previousNode, hookId);
@@ -2620,10 +2712,15 @@ class ShokupanRouter {
2620
2712
  }
2621
2713
  }));
2622
2714
  } else {
2623
- await Promise.all(fns.map((fn) => fn(...args)));
2715
+ return Promise.all(fns.map((fn) => fn(...args)));
2624
2716
  }
2625
2717
  }
2626
2718
  }
2719
+ class RequestContextStore {
2720
+ request;
2721
+ span;
2722
+ }
2723
+ const asyncContext = new AsyncLocalStorage();
2627
2724
  class SystemCpuMonitor {
2628
2725
  constructor(intervalMs = 1e3) {
2629
2726
  this.intervalMs = intervalMs;
@@ -2667,6 +2764,100 @@ class SystemCpuMonitor {
2667
2764
  this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
2668
2765
  }
2669
2766
  }
2767
+ class SurrealDatastore {
2768
+ constructor(db) {
2769
+ this.db = db;
2770
+ process.on("exit", async () => {
2771
+ await this.disconnect();
2772
+ });
2773
+ }
2774
+ createSchema() {
2775
+ this.db.query(`
2776
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
2777
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
2778
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
2779
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
2780
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
2781
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
2782
+ DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
2783
+ `).collect();
2784
+ }
2785
+ /**
2786
+ * Select a record or contents of a table by its ID.
2787
+ */
2788
+ async select(id) {
2789
+ return this.db.select(id);
2790
+ }
2791
+ /**
2792
+ * Merge update data into a record by its ID.
2793
+ */
2794
+ async merge(id, data) {
2795
+ return this.db.update(id).merge(data).catch((err) => {
2796
+ if (err.message.includes("This transaction can be retried")) {
2797
+ return this.db.update(id).merge(data);
2798
+ }
2799
+ throw err;
2800
+ });
2801
+ }
2802
+ /**
2803
+ * Create a record by its ID.
2804
+ */
2805
+ async create(id, data) {
2806
+ return this.db.create(id).content(data).catch((err) => {
2807
+ if (err.message.includes("This transaction can be retried")) {
2808
+ return this.db.create(id).content(data);
2809
+ }
2810
+ throw err;
2811
+ });
2812
+ }
2813
+ /**
2814
+ * Upsert a record by its ID.
2815
+ */
2816
+ async upsert(id, data) {
2817
+ return this.db.upsert(id).content(data).catch((err) => {
2818
+ if (err.message.includes("This transaction can be retried")) {
2819
+ return this.db.upsert(id).content(data);
2820
+ }
2821
+ throw err;
2822
+ });
2823
+ }
2824
+ /**
2825
+ * Delete a record by its ID.
2826
+ */
2827
+ async delete(id) {
2828
+ return this.db.delete(id).catch((err) => {
2829
+ if (err.message.includes("This transaction can be retried")) {
2830
+ return this.db.delete(id);
2831
+ }
2832
+ throw err;
2833
+ });
2834
+ }
2835
+ /**
2836
+ * Run a SurrealDB query.
2837
+ */
2838
+ async query(query, vars) {
2839
+ return this.db.query(query, vars).collect().catch((err) => {
2840
+ if (err.message.includes("This transaction can be retried")) {
2841
+ return this.db.query(query, vars).collect();
2842
+ }
2843
+ throw err;
2844
+ });
2845
+ }
2846
+ /**
2847
+ * Create a relationship between two records.
2848
+ */
2849
+ async relate(fromId, edgeId, toId, data) {
2850
+ return this.db.relate(fromId, edgeId, toId, data).catch((err) => {
2851
+ if (err.message.includes("This transaction can be retried")) {
2852
+ return this.db.relate(fromId, edgeId, toId, data);
2853
+ }
2854
+ throw err;
2855
+ });
2856
+ }
2857
+ disconnect() {
2858
+ return this.db.close();
2859
+ }
2860
+ }
2670
2861
  const defaults = {
2671
2862
  port: 3e3,
2672
2863
  hostname: "localhost",
@@ -2679,9 +2870,15 @@ trace.getTracer("shokupan.application");
2679
2870
  class Shokupan extends ShokupanRouter {
2680
2871
  applicationConfig = {};
2681
2872
  openApiSpec;
2873
+ asyncApiSpec;
2682
2874
  composedMiddleware;
2683
2875
  cpuMonitor;
2684
2876
  server;
2877
+ datastore;
2878
+ dbPromise;
2879
+ get db() {
2880
+ return this.datastore;
2881
+ }
2685
2882
  get logger() {
2686
2883
  return this.applicationConfig.logger;
2687
2884
  }
@@ -2698,6 +2895,19 @@ class Shokupan extends ShokupanRouter {
2698
2895
  line,
2699
2896
  name: "ShokupanApplication"
2700
2897
  };
2898
+ this.dbPromise = this.initDatastore();
2899
+ }
2900
+ async initDatastore() {
2901
+ const db = new Surreal({ engines: this.applicationConfig.surreal?.engines ?? (await import("@surrealdb/node")).createNodeEngines() });
2902
+ this.datastore = new SurrealDatastore(db);
2903
+ await db.connect(
2904
+ this.applicationConfig.surreal?.url ?? (process.env.NODE_ENV === "test" ? "mem://" : "surrealkv://database"),
2905
+ this.applicationConfig.surreal?.connectOptions
2906
+ );
2907
+ await db.use({
2908
+ namespace: this.applicationConfig.surreal?.namespace ?? "vendor",
2909
+ database: this.applicationConfig.surreal?.database ?? "shokupan"
2910
+ });
2701
2911
  }
2702
2912
  /**
2703
2913
  * Adds middleware to the application.
@@ -2777,14 +2987,70 @@ class Shokupan extends ShokupanRouter {
2777
2987
  */
2778
2988
  async listen(port) {
2779
2989
  const finalPort = port ?? this.applicationConfig.port ?? 3e3;
2780
- if (finalPort < 0 || finalPort > 65535) {
2990
+ if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
2781
2991
  throw new Error("Invalid port number");
2782
2992
  }
2783
2993
  await Promise.all(this.startupHooks.map((hook) => hook()));
2784
2994
  if (this.applicationConfig.enableOpenApiGen) {
2785
2995
  this.openApiSpec = await generateOpenApi(this);
2996
+ this.get("/.well-known/openapi.yaml", (ctx) => {
2997
+ try {
2998
+ const yaml = dump(this.openApiSpec);
2999
+ return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
3000
+ } catch (e) {
3001
+ this.logger.error("Failed to generate OpenAPI YAML", { error: e });
3002
+ return ctx.text("Internal Server Error", 500);
3003
+ }
3004
+ });
3005
+ if (this.applicationConfig.aiPlugin?.enabled !== false) {
3006
+ this.get("/.well-known/ai-plugin.json", async (ctx) => {
3007
+ const config = this.applicationConfig.aiPlugin || {};
3008
+ let pkg = {};
3009
+ try {
3010
+ pkg = await Bun.file("package.json").json();
3011
+ } catch (e) {
3012
+ }
3013
+ const manifest = {
3014
+ schema_version: "v1",
3015
+ name_for_human: config.name_for_human || this.openApiSpec.info.title || pkg.name || "Shokupan App",
3016
+ name_for_model: config.name_for_model || this.openApiSpec.info.title || pkg.name || "Shokupan App",
3017
+ description_for_human: config.description_for_human || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
3018
+ description_for_model: config.description_for_model || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
3019
+ auth: config.auth || { type: "none" },
3020
+ api: config.api || {
3021
+ type: "openapi",
3022
+ url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`,
3023
+ is_user_authenticated: false
3024
+ },
3025
+ logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/logo.png`,
3026
+ // Placeholder default
3027
+ contact_email: config.contact_email || pkg.author?.email || "support@example.com",
3028
+ legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/legal`
3029
+ };
3030
+ return ctx.json(manifest);
3031
+ });
3032
+ }
3033
+ if (this.applicationConfig.apiCatalog?.enabled !== false) {
3034
+ this.get("/.well-known/api-catalog", (ctx) => {
3035
+ const config = this.applicationConfig.apiCatalog || {};
3036
+ const catalog = {
3037
+ versions: config.versions || [
3038
+ {
3039
+ name: this.openApiSpec.info.version || "v1",
3040
+ url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/`,
3041
+ spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`
3042
+ }
3043
+ ]
3044
+ };
3045
+ return ctx.json(catalog);
3046
+ });
3047
+ }
2786
3048
  await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
2787
3049
  }
3050
+ if (this.applicationConfig.enableAsyncApiGen) {
3051
+ const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
3052
+ this.asyncApiSpec = await generateAsyncApi2(this);
3053
+ }
2788
3054
  if (port === 0 && process.platform === "linux") ;
2789
3055
  if (this.applicationConfig.autoBackpressureFeedback === true) {
2790
3056
  this.cpuMonitor = new SystemCpuMonitor();
@@ -2844,6 +3110,8 @@ class Shokupan extends ShokupanRouter {
2844
3110
  });
2845
3111
  const ctx = new ShokupanContext(req, self.server);
2846
3112
  ctx[$ws] = ws;
3113
+ ws.data ??= {};
3114
+ ws.data["ctx"] = ctx;
2847
3115
  try {
2848
3116
  await handler(ctx);
2849
3117
  } catch (err) {
@@ -2863,6 +3131,15 @@ class Shokupan extends ShokupanRouter {
2863
3131
  },
2864
3132
  close(ws, code, reason) {
2865
3133
  ws.data?.handler?.close?.(ws, code, reason);
3134
+ const ctx = ws.data?.["ctx"];
3135
+ if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
3136
+ const callbacks = ctx.getDisconnectCallbacks();
3137
+ if (Array.isArray(callbacks) && callbacks.length > 0) {
3138
+ Promise.all(callbacks.map((cb) => cb())).catch((err) => {
3139
+ console.error("Error executing socket disconnect hook:", err);
3140
+ });
3141
+ }
3142
+ }
2866
3143
  }
2867
3144
  }
2868
3145
  };
@@ -2872,7 +3149,6 @@ class Shokupan extends ShokupanRouter {
2872
3149
  factory = createHttpServer();
2873
3150
  }
2874
3151
  this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2875
- console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
2876
3152
  return this.server;
2877
3153
  }
2878
3154
  /**
@@ -3004,9 +3280,6 @@ class Shokupan extends ShokupanRouter {
3004
3280
  await bodyParsing;
3005
3281
  return match.handler(ctx);
3006
3282
  }
3007
- if (ctx.upgrade()) {
3008
- return void 0;
3009
- }
3010
3283
  return null;
3011
3284
  });
3012
3285
  let response;
@@ -3022,6 +3295,9 @@ class Shokupan extends ShokupanRouter {
3022
3295
  } else if (ctx[$routeMatched]) {
3023
3296
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3024
3297
  } else {
3298
+ if (ctx.upgrade()) {
3299
+ return void 0;
3300
+ }
3025
3301
  if (ctx.response.status !== HTTP_STATUS.OK) {
3026
3302
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3027
3303
  } else {
@@ -3034,6 +3310,9 @@ class Shokupan extends ShokupanRouter {
3034
3310
  response = ctx.text(String(result));
3035
3311
  }
3036
3312
  await this.runHooks("onRequestEnd", ctx);
3313
+ if (response instanceof Promise) {
3314
+ response = await response;
3315
+ }
3037
3316
  await this.runHooks("onResponseStart", ctx, response);
3038
3317
  return response;
3039
3318
  } catch (err) {
@@ -3143,8 +3422,7 @@ function RateLimitMiddleware(options = {}) {
3143
3422
  }
3144
3423
  }
3145
3424
  const msg = typeof message === "function" ? message(ctx, key) : message;
3146
- typeof msg === "object" ? JSON.stringify(msg) : String(msg);
3147
- const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
3425
+ const res = await (typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode));
3148
3426
  if (headers) {
3149
3427
  setHeaders(res);
3150
3428
  res.headers.set("Retry-After", String(retryAfter));
@@ -3219,8 +3497,12 @@ function createMethodDecorator(method) {
3219
3497
  }
3220
3498
  target[$routeMethods].set(propertyKey, {
3221
3499
  method,
3222
- path
3500
+ path,
3501
+ source: getCallerInfo(2)
3223
3502
  });
3503
+ if (path.includes("/user")) {
3504
+ console.log(`[Decorator] Captured source for ${propertyKey}:`, getCallerInfo());
3505
+ }
3224
3506
  };
3225
3507
  };
3226
3508
  }
@@ -3243,15 +3525,572 @@ function Event(eventName) {
3243
3525
  function RateLimit(options) {
3244
3526
  return Use(RateLimitMiddleware(options));
3245
3527
  }
3528
+ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3529
+ return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
3530
+ /* @__PURE__ */ jsxs("head", { children: [
3531
+ /* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
3532
+ /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3533
+ /* @__PURE__ */ jsx("title", { children: "Shokupan AsyncAPI" }),
3534
+ /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
3535
+ /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
3536
+ /* @__PURE__ */ jsx("link", { href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap", rel: "stylesheet" }),
3537
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
3538
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
3539
+ /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
3540
+ __html: `
3541
+ window.INITIAL_SPEC = ${JSON.stringify(spec)};
3542
+ window.INITIAL_SERVER_URL = "${serverUrl}";
3543
+ window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
3544
+ `
3545
+ } })
3546
+ ] }),
3547
+ /* @__PURE__ */ jsxs("body", { children: [
3548
+ /* @__PURE__ */ jsxs("div", { class: "app-container", children: [
3549
+ /* @__PURE__ */ jsx(Sidebar, { navTree, disableSourceView }),
3550
+ /* @__PURE__ */ jsx("div", { class: "resizer", id: "resizer-left" }),
3551
+ /* @__PURE__ */ jsx(MainContent, {}),
3552
+ /* @__PURE__ */ jsx("div", { class: "resizer", id: "resizer-right" }),
3553
+ /* @__PURE__ */ jsx(ConsolePanel, { serverUrl })
3554
+ ] }),
3555
+ /* @__PURE__ */ jsx("script", { src: "https://cdn.socket.io/4.7.4/socket.io.min.js" }),
3556
+ /* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
3557
+ /* @__PURE__ */ jsx("script", { src: `${base}/asyncapi-client.mjs`, type: "module" })
3558
+ ] })
3559
+ ] });
3560
+ }
3561
+ function Sidebar({ navTree, disableSourceView }) {
3562
+ return /* @__PURE__ */ jsxs("div", { class: "sidebar scroller", id: "sidebar", children: [
3563
+ /* @__PURE__ */ jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
3564
+ /* @__PURE__ */ jsx("h2", { children: "AsyncAPI" }),
3565
+ /* @__PURE__ */ jsx("button", { id: "btn-collapse-nav", class: "btn-icon", title: "Collapse Sidebar", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) }) })
3566
+ ] }),
3567
+ /* @__PURE__ */ jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
3568
+ ] });
3569
+ }
3570
+ function NavNode({ node, level, disableSourceView }) {
3571
+ const sortedEntries = Object.entries(node.children || {}).sort((a, b) => {
3572
+ const [aKey, aItem] = a;
3573
+ const [bKey, bItem] = b;
3574
+ const isWarningA = aItem.data?.op?.["x-warning"];
3575
+ const isWarningB = bItem.data?.op?.["x-warning"];
3576
+ if (isWarningA && !isWarningB) return -1;
3577
+ if (!isWarningA && isWarningB) return 1;
3578
+ if (aKey === bKey) return 0;
3579
+ if (aKey === "Warning" || aKey === "Warnings") return -1;
3580
+ if (bKey === "Warning" || bKey === "Warnings") return 1;
3581
+ if (aKey === "Application") return -1;
3582
+ if (bKey === "Application") return 1;
3583
+ if (aKey[0] === "/") return 1;
3584
+ if (bKey[0] === "/") return -1;
3585
+ return aKey.localeCompare(bKey);
3586
+ });
3587
+ return /* @__PURE__ */ jsx(Fragment, { children: sortedEntries.map(([key, item]) => {
3588
+ const hasChildren = Object.keys(item.children || {}).length > 0;
3589
+ if (level === 0) {
3590
+ return /* @__PURE__ */ jsxs("div", { children: [
3591
+ /* @__PURE__ */ jsx("div", { class: "group-label", children: key }),
3592
+ hasChildren && /* @__PURE__ */ jsx("div", { class: "tree-node", style: "margin-left: 0", children: /* @__PURE__ */ jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
3593
+ ] }, key);
3594
+ }
3595
+ const isLeaf = item.isLeaf;
3596
+ return /* @__PURE__ */ jsxs("div", { children: [
3597
+ isLeaf ? /* @__PURE__ */ jsx(LeafNode, { item, label: key, disableSourceView }) : /* @__PURE__ */ jsx("div", { class: "tree-item", style: "color: var(--text-muted)", children: /* @__PURE__ */ jsx("span", { class: "tree-label", children: key }) }),
3598
+ hasChildren && /* @__PURE__ */ jsx("div", { class: "tree-node", children: /* @__PURE__ */ jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
3599
+ ] }, key);
3600
+ }) });
3601
+ }
3602
+ function LeafNode({ item, label, disableSourceView }) {
3603
+ const isWarning = item.data?.op?.["x-warning"];
3604
+ const opId = item.data?.name;
3605
+ const sourceInfo = item.data?.op?.["x-source-info"];
3606
+ let content;
3607
+ if (isWarning) {
3608
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
3609
+ /* @__PURE__ */ jsx("span", { style: "margin-right: 6px;", children: "⚠️" }),
3610
+ /* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
3611
+ ] });
3612
+ } else {
3613
+ const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
3614
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
3615
+ /* @__PURE__ */ jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
3616
+ /* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
3617
+ ] });
3618
+ }
3619
+ return /* @__PURE__ */ jsxs("div", { class: "tree-item", "data-event": opId, style: isWarning ? "color: #fbbf24" : "", children: [
3620
+ content,
3621
+ sourceInfo && !disableSourceView && /* @__PURE__ */ jsx(
3622
+ "a",
3623
+ {
3624
+ href: `vscode://file/${sourceInfo.file}:${sourceInfo.line}`,
3625
+ class: "source-link",
3626
+ onClick: (e) => {
3627
+ e.stopPropagation();
3628
+ },
3629
+ title: `${sourceInfo.file}:${sourceInfo.line}`,
3630
+ children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", style: "display:block", children: [
3631
+ /* @__PURE__ */ jsx("polyline", { points: "16 18 22 12 16 6" }),
3632
+ /* @__PURE__ */ jsx("polyline", { points: "8 6 2 12 8 18" })
3633
+ ] })
3634
+ }
3635
+ )
3636
+ ] });
3637
+ }
3638
+ function MainContent() {
3639
+ return /* @__PURE__ */ jsxs("div", { id: "main-wrapper", style: "flex: 1; min-width: 0; position: relative; overflow: hidden;", children: [
3640
+ /* @__PURE__ */ jsx("button", { id: "btn-expand-nav", class: "btn-icon floating-toggle left", title: "Expand Sidebar", style: "display:none;", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
3641
+ /* @__PURE__ */ jsx("button", { id: "btn-expand-console", class: "btn-icon floating-toggle right", title: "Expand Console", style: "display:none;", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) }) }),
3642
+ /* @__PURE__ */ jsx("main", { class: "main-content scroller", id: "doc-panel", style: "height: 100%;", children: /* @__PURE__ */ jsxs("div", { class: "empty-state", children: [
3643
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", children: [
3644
+ /* @__PURE__ */ jsx("path", { d: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" }),
3645
+ /* @__PURE__ */ jsx("path", { d: "M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" })
3646
+ ] }),
3647
+ /* @__PURE__ */ jsx("h3", { children: "Select an event to view details" })
3648
+ ] }) })
3649
+ ] });
3650
+ }
3651
+ function ConsolePanel({ serverUrl }) {
3652
+ return /* @__PURE__ */ jsxs("div", { class: "console-panel", id: "console-panel", children: [
3653
+ /* @__PURE__ */ jsxs("div", { class: "console-header", children: [
3654
+ /* @__PURE__ */ jsxs("div", { style: "display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;", children: [
3655
+ /* @__PURE__ */ jsx("h3", { style: "margin:0; font-size:1rem;", children: "Console" }),
3656
+ /* @__PURE__ */ jsxs("div", { style: "display:flex; gap: 4px;", children: [
3657
+ /* @__PURE__ */ jsx("button", { id: "btn-maximize-console", class: "btn-icon", title: "Maximize Console", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }) }) }),
3658
+ /* @__PURE__ */ jsx("button", { id: "btn-collapse-console", class: "btn-icon", title: "Collapse Console", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) })
3659
+ ] })
3660
+ ] }),
3661
+ /* @__PURE__ */ jsxs("div", { class: "connection-bar", children: [
3662
+ /* @__PURE__ */ jsxs("select", { id: "protocol", children: [
3663
+ /* @__PURE__ */ jsx("option", { value: "ws", children: "WS" }),
3664
+ /* @__PURE__ */ jsx("option", { value: "wss", children: "WSS" }),
3665
+ /* @__PURE__ */ jsx("option", { value: "socket.io", children: "Socket.IO" })
3666
+ ] }),
3667
+ /* @__PURE__ */ jsx("div", { style: "width: 1px; background: rgba(255,255,255,0.1); margin: 2px 0;" }),
3668
+ /* @__PURE__ */ jsx("input", { type: "text", id: "url", value: serverUrl })
3669
+ ] }),
3670
+ /* @__PURE__ */ jsxs("div", { style: "display: grid; grid-template-columns: 1fr auto; gap: 8px;", children: [
3671
+ /* @__PURE__ */ jsx("button", { id: "connect-btn", class: "btn", children: "Connect" }),
3672
+ /* @__PURE__ */ jsx("button", { id: "clear-logs-btn", class: "btn secondary", title: "Clear Logs", children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("path", { d: "M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }) }) })
3673
+ ] }),
3674
+ /* @__PURE__ */ jsxs("div", { class: "status-indicator", children: [
3675
+ /* @__PURE__ */ jsx("div", { id: "status-dot", class: "dot" }),
3676
+ /* @__PURE__ */ jsx("span", { id: "connection-status", children: "Disconnected" })
3677
+ ] })
3678
+ ] }),
3679
+ /* @__PURE__ */ jsx("div", { class: "logs-container scroller", id: "logs", children: /* @__PURE__ */ jsx("div", { class: "log-shim", id: "log-shim" }) }),
3680
+ /* @__PURE__ */ jsxs("div", { class: "compose-area", children: [
3681
+ /* @__PURE__ */ jsxs("div", { class: "compose-header", children: [
3682
+ /* @__PURE__ */ jsx("span", { children: "Payload" }),
3683
+ /* @__PURE__ */ jsx("span", { id: "target-event", style: "color: var(--primary);", children: "--" })
3684
+ ] }),
3685
+ /* @__PURE__ */ jsx("div", { id: "editor-container" }),
3686
+ /* @__PURE__ */ jsx("div", { class: "send-bar", children: /* @__PURE__ */ jsx("button", { id: "send-btn", class: "btn", children: "Send Message" }) })
3687
+ ] })
3688
+ ] });
3689
+ }
3690
+ function buildNavTree(spec) {
3691
+ if (!spec || !spec.channels) return { children: {} };
3692
+ const root = { children: {} };
3693
+ Object.keys(spec.channels).forEach((name) => {
3694
+ const ch = spec.channels[name];
3695
+ const op = ch.publish || ch.subscribe;
3696
+ const type = ch.publish ? "publish" : "subscribe";
3697
+ const tag = op.tags && op.tags.length > 0 ? op.tags[0].name : "General";
3698
+ if (!root.children[tag]) root.children[tag] = { children: {} };
3699
+ const parts = name.split(/[\.\/]/);
3700
+ let current = root.children[tag];
3701
+ parts.forEach((part, i) => {
3702
+ if (!current.children[part]) current.children[part] = { children: {} };
3703
+ current = current.children[part];
3704
+ if (i === parts.length - 1) {
3705
+ current.isLeaf = true;
3706
+ current.data = { name, op, type };
3707
+ }
3708
+ });
3709
+ });
3710
+ return root;
3711
+ }
3712
+ async function getAstRoutes(applications) {
3713
+ const astRoutes = [];
3714
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
3715
+ if (seen.has(app.name)) return [];
3716
+ const newSeen = new Set(seen);
3717
+ newSeen.add(app.name);
3718
+ const expanded = [];
3719
+ for (const route of app.routes) {
3720
+ expanded.push({
3721
+ ...route,
3722
+ // For events, path is the event name
3723
+ path: route.path.startsWith("/") ? route.path.slice(1) : route.path
3724
+ });
3725
+ }
3726
+ if (app.mounted) {
3727
+ for (const mount of app.mounted) {
3728
+ const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
3729
+ if (targetApp) {
3730
+ expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
3731
+ }
3732
+ }
3733
+ }
3734
+ return expanded;
3735
+ };
3736
+ applications.forEach((app) => {
3737
+ astRoutes.push(...getExpandedRoutes(app));
3738
+ });
3739
+ return astRoutes;
3740
+ }
3741
+ async function generateAsyncApi(rootRouter, options = {}) {
3742
+ const channels = {};
3743
+ let astRoutes = [];
3744
+ try {
3745
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
3746
+ const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
3747
+ const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
3748
+ const { applications } = await analyzer.analyze();
3749
+ astRoutes = await getAstRoutes(applications);
3750
+ } catch (e) {
3751
+ }
3752
+ const matchedAstRoutes = /* @__PURE__ */ new Set();
3753
+ const collect = async (router, prefix = "") => {
3754
+ const eventHandlers = router.getEventHandlers();
3755
+ let routerTag = "Other";
3756
+ if (router[$isApplication]) {
3757
+ routerTag = "Application";
3758
+ } else if (router.constructor.name && router.constructor.name !== "ShokupanRouter") {
3759
+ routerTag = router.constructor.name;
3760
+ } else {
3761
+ routerTag = router[$mountPath] || "Router";
3762
+ }
3763
+ if (eventHandlers) {
3764
+ for (const [eventName, handlers] of eventHandlers.entries()) {
3765
+ for (const handler of handlers) {
3766
+ const userSpec = handler.spec;
3767
+ let tags = userSpec?.tags;
3768
+ if (!tags && routerTag) {
3769
+ tags = [{ name: routerTag }];
3770
+ }
3771
+ let astMatch = astRoutes.find(
3772
+ (r) => (r.method === "EVENT" || r.method === "ON") && r.path === eventName
3773
+ );
3774
+ if (!astMatch) {
3775
+ const runtimeSource = (handler.originalHandler || handler).toString();
3776
+ const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
3777
+ const normalize = (s) => stripComments(s).replace(/\s+/g, "");
3778
+ const runtimeHandlerSrc = normalize(runtimeSource);
3779
+ const eventRoutes = astRoutes.filter((r) => r.method === "EVENT" || r.method === "ON");
3780
+ astMatch = eventRoutes.find((r) => {
3781
+ const astHandlerSrc = normalize(r.handlerSource || r.handlerName || "");
3782
+ if (!astHandlerSrc || astHandlerSrc.length < 5) return false;
3783
+ return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(normalize(r.handlerSource).substring(0, 50));
3784
+ });
3785
+ }
3786
+ if (astMatch) matchedAstRoutes.add(astMatch);
3787
+ const sourceInfo = handler.source || astMatch?.sourceContext ? {
3788
+ file: handler.source?.file || astMatch?.sourceContext?.file,
3789
+ line: handler.source?.line || astMatch?.sourceContext?.startLine,
3790
+ startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
3791
+ endLine: astMatch?.sourceContext?.endLine,
3792
+ highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
3793
+ } : void 0;
3794
+ if (!channels[eventName]) {
3795
+ channels[eventName] = {
3796
+ publish: {
3797
+ operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
3798
+ tags,
3799
+ message: {
3800
+ payload: { type: "object" },
3801
+ ...userSpec?.message ? userSpec.message : {}
3802
+ },
3803
+ ...userSpec?.type === "publish" ? userSpec : {},
3804
+ "x-source-info": sourceInfo ? [sourceInfo] : [],
3805
+ "x-shokupan-source": sourceInfo
3806
+ // Simplified
3807
+ }
3808
+ };
3809
+ if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
3810
+ if (userSpec?.description) channels[eventName].publish.description = userSpec.description;
3811
+ } else {
3812
+ if (sourceInfo) {
3813
+ if (!channels[eventName].publish["x-source-info"]) {
3814
+ channels[eventName].publish["x-source-info"] = [];
3815
+ }
3816
+ const exists = channels[eventName].publish["x-source-info"].some(
3817
+ (s) => s.file === sourceInfo.file && s.line === sourceInfo.line
3818
+ );
3819
+ if (!exists) {
3820
+ channels[eventName].publish["x-source-info"].push(sourceInfo);
3821
+ }
3822
+ }
3823
+ }
3824
+ let emits = astMatch?.emits || [];
3825
+ for (const emit of emits) {
3826
+ if (emit.event === "__DYNAMIC_EMIT__") {
3827
+ const warningKey = `${eventName}/Dynamic Emit`;
3828
+ channels[warningKey] = {
3829
+ subscribe: {
3830
+ operationId: `dynamicEmitWarning${eventName}`,
3831
+ summary: "Dynamic Emit Detected",
3832
+ description: "This handler emits an event with a dynamic name that could not be determined statically.",
3833
+ tags,
3834
+ "x-warning": true,
3835
+ "x-source-info": {
3836
+ file: astMatch?.sourceContext?.file,
3837
+ line: emit.location?.startLine,
3838
+ startLine: emit.location?.startLine,
3839
+ endLine: emit.location?.endLine,
3840
+ highlightLines: emit.location ? [emit.location.startLine, emit.location.endLine] : void 0
3841
+ },
3842
+ "x-shokupan-source": {
3843
+ file: astMatch?.sourceContext?.file,
3844
+ line: emit.location?.startLine
3845
+ },
3846
+ message: { payload: { type: "object" } }
3847
+ }
3848
+ };
3849
+ continue;
3850
+ }
3851
+ const emitStart = emit.location?.startLine;
3852
+ const emitEnd = emit.location?.endLine;
3853
+ const newSourceInfo = sourceInfo && emitStart ? {
3854
+ file: sourceInfo.file,
3855
+ line: emitStart,
3856
+ startLine: emitStart,
3857
+ endLine: emitEnd,
3858
+ highlightLines: sourceInfo.highlightLines,
3859
+ emitHighlightLines: [emitStart, emitEnd]
3860
+ } : void 0;
3861
+ if (!channels[emit.event]) {
3862
+ channels[emit.event] = {
3863
+ subscribe: {
3864
+ operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
3865
+ tags,
3866
+ message: {
3867
+ payload: emit.payload || { type: "object" }
3868
+ },
3869
+ "x-source-info": newSourceInfo ? [newSourceInfo] : [],
3870
+ "x-shokupan-source": sourceInfo && emitStart ? {
3871
+ file: sourceInfo.file,
3872
+ line: emitStart
3873
+ } : void 0
3874
+ }
3875
+ };
3876
+ } else {
3877
+ if (newSourceInfo) {
3878
+ if (!channels[emit.event].subscribe["x-source-info"]) {
3879
+ channels[emit.event].subscribe["x-source-info"] = [];
3880
+ }
3881
+ const existing = channels[emit.event].subscribe["x-source-info"];
3882
+ const exists = existing.some(
3883
+ (s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
3884
+ );
3885
+ if (!exists) {
3886
+ existing.push(newSourceInfo);
3887
+ }
3888
+ }
3889
+ }
3890
+ }
3891
+ }
3892
+ }
3893
+ }
3894
+ const httpRoutes = router[$routes];
3895
+ if (httpRoutes) {
3896
+ for (const route of httpRoutes) {
3897
+ const handler = route.handler;
3898
+ let tags = route.handlerSpec?.tags;
3899
+ if (!tags && routerTag) {
3900
+ tags = [{ name: routerTag }];
3901
+ }
3902
+ const methodUpper = route.method.toUpperCase();
3903
+ let astMatch = astRoutes.find(
3904
+ (r) => r.method === methodUpper && (r.path === route.path || r.path === "/" + route.path)
3905
+ );
3906
+ if (!astMatch) {
3907
+ const runtimeSource = (handler.originalHandler || handler).toString();
3908
+ const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
3909
+ const sameMethodRoutes = astRoutes.filter((r) => r.method === methodUpper);
3910
+ astMatch = sameMethodRoutes.find((r) => {
3911
+ const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
3912
+ if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
3913
+ return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
3914
+ });
3915
+ }
3916
+ const sourceInfo = handler.source || astMatch?.sourceContext ? {
3917
+ file: handler.source?.file || astMatch?.sourceContext?.file,
3918
+ line: handler.source?.line || astMatch?.sourceContext?.startLine,
3919
+ startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
3920
+ endLine: astMatch?.sourceContext?.endLine,
3921
+ highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
3922
+ } : void 0;
3923
+ let emits = astMatch?.emits || [];
3924
+ for (const emit of emits) {
3925
+ const emitStart = emit.location?.startLine;
3926
+ const emitEnd = emit.location?.endLine;
3927
+ const newSourceInfo = sourceInfo && emitStart ? {
3928
+ file: sourceInfo.file,
3929
+ line: emitStart,
3930
+ startLine: emitStart,
3931
+ endLine: emitEnd,
3932
+ highlightLines: sourceInfo.highlightLines,
3933
+ emitHighlightLines: [emitStart, emitEnd]
3934
+ } : void 0;
3935
+ if (!channels[emit.event]) {
3936
+ channels[emit.event] = {
3937
+ subscribe: {
3938
+ operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
3939
+ tags,
3940
+ message: {
3941
+ payload: emit.payload || { type: "object" }
3942
+ },
3943
+ "x-source-info": newSourceInfo ? [newSourceInfo] : [],
3944
+ "x-shokupan-source": sourceInfo && emitStart ? {
3945
+ file: sourceInfo.file,
3946
+ line: emitStart
3947
+ } : void 0
3948
+ }
3949
+ };
3950
+ } else {
3951
+ if (newSourceInfo) {
3952
+ if (!channels[emit.event].subscribe["x-source-info"]) {
3953
+ channels[emit.event].subscribe["x-source-info"] = [];
3954
+ }
3955
+ const existing = channels[emit.event].subscribe["x-source-info"];
3956
+ const exists = existing.some(
3957
+ (s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
3958
+ );
3959
+ if (!exists) {
3960
+ existing.push(newSourceInfo);
3961
+ }
3962
+ }
3963
+ }
3964
+ }
3965
+ }
3966
+ }
3967
+ const childRouters = router[$childRouters];
3968
+ for (const child of childRouters) {
3969
+ await collect(child);
3970
+ }
3971
+ };
3972
+ await collect(rootRouter);
3973
+ const dynamicEvents = astRoutes.filter((r) => r.path === "__DYNAMIC_EVENT__" && !matchedAstRoutes.has(r));
3974
+ dynamicEvents.forEach((r, i) => {
3975
+ let prefix = "Anonymous";
3976
+ if (r.handlerName && !r.handlerName.includes("=>") && !r.handlerName.includes("{")) {
3977
+ const parts = r.handlerName.split(".");
3978
+ if (parts.length > 0) prefix = parts[0];
3979
+ }
3980
+ const key = `${prefix}.Dynamic Event ${i + 1}`;
3981
+ channels[key] = {
3982
+ publish: {
3983
+ operationId: `dynamicEventWarning${i}`,
3984
+ summary: "Dynamic Event Detected",
3985
+ description: `A dynamic event listener was detected in your source code but the event name could not be determined statically.`,
3986
+ tags: [{ name: "Warnings" }],
3987
+ "x-warning": true,
3988
+ "x-source-info": {
3989
+ file: r.sourceContext?.file,
3990
+ line: r.sourceContext?.startLine,
3991
+ startLine: r.sourceContext?.startLine,
3992
+ endLine: r.sourceContext?.endLine,
3993
+ highlightLines: r.sourceContext ? [r.sourceContext.startLine, r.sourceContext.endLine] : void 0
3994
+ },
3995
+ "x-shokupan-source": {
3996
+ file: r.sourceContext?.file,
3997
+ line: r.sourceContext?.startLine
3998
+ },
3999
+ message: { payload: { type: "object" } }
4000
+ }
4001
+ };
4002
+ });
4003
+ return {
4004
+ asyncapi: "3.0.0",
4005
+ info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
4006
+ channels
4007
+ };
4008
+ }
4009
+ const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
4010
+ __proto__: null,
4011
+ generateAsyncApi
4012
+ }, Symbol.toStringTag, { value: "Module" }));
4013
+ class AsyncApiPlugin extends ShokupanRouter {
4014
+ constructor(pluginOptions = {}) {
4015
+ super({ renderer: renderToString });
4016
+ this.pluginOptions = pluginOptions;
4017
+ this.pluginOptions.path ??= "/asyncapi";
4018
+ this.init();
4019
+ }
4020
+ static getBasePath() {
4021
+ const dir = dirname(fileURLToPath(import.meta.url));
4022
+ if (dir.endsWith("dist")) {
4023
+ return dir + "/plugins/application/asyncapi";
4024
+ }
4025
+ return dir;
4026
+ }
4027
+ onInit(app, options) {
4028
+ const path = this.pluginOptions.path || options?.path || "/asyncapi";
4029
+ app.mount(path, this);
4030
+ if (app.applicationConfig.enableAsyncApiGen !== true) {
4031
+ console.warn("AsyncApiPlugin: enableAsyncApiGen is disabled. AsyncApiPlugin will not generate spec.");
4032
+ }
4033
+ }
4034
+ init() {
4035
+ const serveFile = async (ctx, file, type) => {
4036
+ const content = await readFile(join$1(AsyncApiPlugin.getBasePath(), "static", file), "utf-8");
4037
+ ctx.set("Content-Type", type);
4038
+ return ctx.send(content);
4039
+ };
4040
+ this.get("/style.css", (ctx) => serveFile(ctx, "style.css", "text/css"));
4041
+ this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
4042
+ this.get("/asyncapi-client.mjs", (ctx) => serveFile(ctx, "asyncapi-client.mjs", "application/javascript"));
4043
+ this.get("/", async (ctx) => {
4044
+ let spec = ctx.app?.asyncApiSpec;
4045
+ if (!spec) {
4046
+ spec = await generateAsyncApi(ctx.app);
4047
+ }
4048
+ if (this.pluginOptions.spec) {
4049
+ deepMerge(spec, this.pluginOptions.spec);
4050
+ }
4051
+ const serverUrl = `${ctx.hostname}:${ctx.app?.applicationConfig.port}`;
4052
+ const base = this.pluginOptions.path;
4053
+ const disableSourceView = this.pluginOptions.disableSourceView;
4054
+ const navTree = buildNavTree(spec);
4055
+ return ctx.jsx(AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }));
4056
+ });
4057
+ this.get("/json", async (ctx) => {
4058
+ let spec = ctx.app?.asyncApiSpec;
4059
+ if (!spec) {
4060
+ spec = await generateAsyncApi(ctx.app);
4061
+ }
4062
+ if (this.pluginOptions.spec) {
4063
+ deepMerge(spec, this.pluginOptions.spec);
4064
+ }
4065
+ return ctx.json(spec);
4066
+ });
4067
+ this.get("/_code", async (ctx) => {
4068
+ const file = ctx.query["file"];
4069
+ if (!file || typeof file !== "string") {
4070
+ return ctx.text("Missing file parameter", 400);
4071
+ }
4072
+ try {
4073
+ const content = await readFile(file, "utf8");
4074
+ return ctx.text(content);
4075
+ } catch (e) {
4076
+ return ctx.text("File not found: " + e.message, 404);
4077
+ }
4078
+ });
4079
+ }
4080
+ }
3246
4081
  class AuthPlugin extends ShokupanRouter {
3247
4082
  constructor(authConfig) {
3248
4083
  super();
3249
4084
  this.authConfig = authConfig;
3250
4085
  this.secret = typeof authConfig.jwtSecret === "string" ? new TextEncoder().encode(authConfig.jwtSecret) : authConfig.jwtSecret;
3251
- this.init();
3252
4086
  }
3253
4087
  secret;
3254
- onInit(app, options) {
4088
+ arctic;
4089
+ jose;
4090
+ async onInit(app, options) {
4091
+ this.arctic = await import("arctic");
4092
+ this.jose = await import("jose");
4093
+ this.init();
3255
4094
  if (options?.path) {
3256
4095
  app.mount(options.path, this);
3257
4096
  } else {
@@ -3259,6 +4098,7 @@ class AuthPlugin extends ShokupanRouter {
3259
4098
  }
3260
4099
  }
3261
4100
  getProviderInstance(name, p) {
4101
+ const { GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
3262
4102
  switch (name) {
3263
4103
  case "github":
3264
4104
  return new GitHub(p.clientId, p.clientSecret, p.redirectUri);
@@ -3286,7 +4126,7 @@ class AuthPlugin extends ShokupanRouter {
3286
4126
  }
3287
4127
  async createSession(user, ctx) {
3288
4128
  const alg = "HS256";
3289
- const jwt = await new jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
4129
+ const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
3290
4130
  const opts = this.authConfig.cookieOptions || {};
3291
4131
  let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
3292
4132
  if (opts.secure) cookie += "; Secure";
@@ -3296,6 +4136,7 @@ class AuthPlugin extends ShokupanRouter {
3296
4136
  return jwt;
3297
4137
  }
3298
4138
  init() {
4139
+ const { generateState, generateCodeVerifier, GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
3299
4140
  const providerEntries = Object.entries(this.authConfig.providers);
3300
4141
  for (let i = 0; i < providerEntries.length; i++) {
3301
4142
  const [providerName, providerConfig] = providerEntries[i];
@@ -3427,7 +4268,7 @@ class AuthPlugin extends ShokupanRouter {
3427
4268
  };
3428
4269
  } else if (provider === "apple") {
3429
4270
  if (idToken) {
3430
- const payload = jose.decodeJwt(idToken);
4271
+ const payload = this.jose.decodeJwt(idToken);
3431
4272
  user = {
3432
4273
  id: payload.sub,
3433
4274
  email: payload["email"],
@@ -3458,6 +4299,9 @@ class AuthPlugin extends ShokupanRouter {
3458
4299
  */
3459
4300
  getMiddleware() {
3460
4301
  return async (ctx, next) => {
4302
+ if (!this.jose) {
4303
+ this.jose = await import("jose");
4304
+ }
3461
4305
  const authHeader = ctx.req.headers.get("Authorization");
3462
4306
  let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
3463
4307
  if (!token) {
@@ -3466,7 +4310,7 @@ class AuthPlugin extends ShokupanRouter {
3466
4310
  }
3467
4311
  if (token) {
3468
4312
  try {
3469
- const { payload } = await jose.jwtVerify(token, this.secret);
4313
+ const { payload } = await this.jose.jwtVerify(token, this.secret);
3470
4314
  ctx.user = payload;
3471
4315
  } catch {
3472
4316
  }
@@ -3583,6 +4427,187 @@ class ClusterPlugin {
3583
4427
  }
3584
4428
  }
3585
4429
  }
4430
+ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
4431
+ return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
4432
+ /* @__PURE__ */ jsxs("head", { children: [
4433
+ /* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
4434
+ /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
4435
+ /* @__PURE__ */ jsx("title", { children: "Shokupan Debug Dashboard" }),
4436
+ /* @__PURE__ */ jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
4437
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
4438
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
4439
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/styles.css` }),
4440
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/reactflow.css` }),
4441
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
4442
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
4443
+ /* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
4444
+ /* @__PURE__ */ jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
4445
+ ] }),
4446
+ /* @__PURE__ */ jsxs("body", { children: [
4447
+ /* @__PURE__ */ jsxs("div", { class: "container", children: [
4448
+ /* @__PURE__ */ jsxs("header", { children: [
4449
+ /* @__PURE__ */ jsxs("div", { children: [
4450
+ /* @__PURE__ */ jsx("h1", { children: "Dashboard" }),
4451
+ /* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary)", children: [
4452
+ "Uptime: ",
4453
+ /* @__PURE__ */ jsx("span", { id: "uptime", children: uptime })
4454
+ ] })
4455
+ ] }),
4456
+ /* @__PURE__ */ jsxs("div", { class: "tabs", children: [
4457
+ /* @__PURE__ */ jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
4458
+ /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
4459
+ /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
4460
+ /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
4461
+ /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
4462
+ integrations.scalar && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
4463
+ integrations.asyncapi && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
4464
+ ] })
4465
+ ] }),
4466
+ /* @__PURE__ */ jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
4467
+ /* @__PURE__ */ jsx(MetricsGrid, { metrics }),
4468
+ /* @__PURE__ */ jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
4469
+ /* @__PURE__ */ jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxs("select", { id: "time-range-selector", onchange: "updateCharts(); updateDashboard(); fetchTopStats();", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px; border-radius: 4px;", children: [
4470
+ /* @__PURE__ */ jsx("option", { value: "1m", children: "1 Minute" }),
4471
+ /* @__PURE__ */ jsx("option", { value: "5m", children: "5 Minutes" }),
4472
+ /* @__PURE__ */ jsx("option", { value: "30m", children: "30 Minutes" }),
4473
+ /* @__PURE__ */ jsx("option", { value: "1h", children: "1 Hour" }),
4474
+ /* @__PURE__ */ jsx("option", { value: "2h", children: "2 Hours" }),
4475
+ /* @__PURE__ */ jsx("option", { value: "6h", children: "6 Hours" }),
4476
+ /* @__PURE__ */ jsx("option", { value: "12h", children: "12 Hours" }),
4477
+ /* @__PURE__ */ jsx("option", { value: "1d", children: "1 Day" }),
4478
+ /* @__PURE__ */ jsx("option", { value: "3d", children: "3 Days" }),
4479
+ /* @__PURE__ */ jsx("option", { value: "7d", children: "7 Days" }),
4480
+ /* @__PURE__ */ jsx("option", { value: "30d", children: "30 Days" })
4481
+ ] }) }),
4482
+ /* @__PURE__ */ jsxs("div", { class: "card-container", children: [
4483
+ /* @__PURE__ */ jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
4484
+ /* @__PURE__ */ jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
4485
+ /* @__PURE__ */ jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
4486
+ /* @__PURE__ */ jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
4487
+ /* @__PURE__ */ jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
4488
+ /* @__PURE__ */ jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
4489
+ /* @__PURE__ */ jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
4490
+ ] }),
4491
+ /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
4492
+ /* @__PURE__ */ jsxs("div", { class: "card-container", children: [
4493
+ /* @__PURE__ */ jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
4494
+ /* @__PURE__ */ jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
4495
+ /* @__PURE__ */ jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
4496
+ /* @__PURE__ */ jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
4497
+ ] }),
4498
+ /* @__PURE__ */ jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsx("div", { id: "requests-table", class: "table-dark" }) })
4499
+ ] })
4500
+ ] }),
4501
+ /* @__PURE__ */ jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
4502
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Component Registry" }),
4503
+ /* @__PURE__ */ jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
4504
+ ] }) }),
4505
+ /* @__PURE__ */ jsxs("div", { id: "tab-graph", class: "tab-content", children: [
4506
+ /* @__PURE__ */ jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsx("input", { type: "text", id: "graph-search", placeholder: "Search routes or middleware...", "aria-label": "Search routes or middleware", style: "flex:1; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg-primary); border: 1px solid var(--card-border); color: var(--text-primary);" }) }) }),
4507
+ /* @__PURE__ */ jsx("div", { id: "cy" })
4508
+ ] }),
4509
+ /* @__PURE__ */ jsxs("div", { id: "tab-requests", class: "tab-content", children: [
4510
+ /* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4511
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
4512
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" }) })
4513
+ ] }),
4514
+ /* @__PURE__ */ jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
4515
+ /* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
4516
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Request Details" }),
4517
+ /* @__PURE__ */ jsx("div", { id: "request-details-content" }),
4518
+ /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
4519
+ /* @__PURE__ */ jsx("div", { id: "middleware-trace-container" })
4520
+ ] })
4521
+ ] }),
4522
+ /* @__PURE__ */ jsxs("div", { id: "tab-failures", class: "tab-content", children: [
4523
+ /* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4524
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
4525
+ /* @__PURE__ */ jsxs("div", { children: [
4526
+ /* @__PURE__ */ jsx("button", { onclick: "importFailure()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-right: 8px;", children: "Import" }),
4527
+ /* @__PURE__ */ jsx("button", { onclick: "fetchFailures()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" })
4528
+ ] })
4529
+ ] }),
4530
+ /* @__PURE__ */ jsx("div", { id: "failures-table-container" })
4531
+ ] }),
4532
+ integrations.scalar && /* @__PURE__ */ jsx("div", { id: "tab-scalar", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
4533
+ integrations.asyncapi && /* @__PURE__ */ jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
4534
+ ] }),
4535
+ /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
4536
+ __html: `
4537
+ // Injected function from server config
4538
+ const getRequestHeaders = ${getRequestHeadersSource};
4539
+ `
4540
+ } }),
4541
+ /* @__PURE__ */ jsx("script", { src: `${base}/poll.js` }),
4542
+ /* @__PURE__ */ jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
4543
+ /* @__PURE__ */ jsx("script", { src: `${base}/charts.js` }),
4544
+ /* @__PURE__ */ jsx("script", { src: `${base}/tables.js` }),
4545
+ /* @__PURE__ */ jsx("script", { src: `${base}/registry.js` }),
4546
+ /* @__PURE__ */ jsx("script", { src: `${base}/failures.js` }),
4547
+ /* @__PURE__ */ jsx("script", { src: `${base}/requests.js` }),
4548
+ /* @__PURE__ */ jsx("script", { src: `${base}/tabs.js` })
4549
+ ] })
4550
+ ] });
4551
+ }
4552
+ function MetricsGrid({ metrics }) {
4553
+ const total = metrics.totalRequests;
4554
+ const active = metrics.activeRequests;
4555
+ const finished = total - active;
4556
+ const successRate = finished ? Math.round(metrics.successfulRequests / finished * 100) : 100;
4557
+ const failRate = finished ? Math.round(metrics.failedRequests / finished * 100) : 0;
4558
+ return /* @__PURE__ */ jsxs("div", { class: "metrics-grid", children: [
4559
+ /* @__PURE__ */ jsxs("div", { class: "card", children: [
4560
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Total Requests" }),
4561
+ /* @__PURE__ */ jsx("div", { class: "card-value", id: "total-requests", children: metrics.totalRequests })
4562
+ ] }),
4563
+ /* @__PURE__ */ jsxs("div", { class: "card", children: [
4564
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Active Requests" }),
4565
+ /* @__PURE__ */ jsx("div", { class: "card-value", style: "color: var(--accent)", id: "active-requests", children: metrics.activeRequests })
4566
+ ] }),
4567
+ /* @__PURE__ */ jsxs("div", { class: "card", children: [
4568
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Success Rate" }),
4569
+ /* @__PURE__ */ jsx("div", { class: "card-value text-success", children: /* @__PURE__ */ jsxs("span", { id: "success-rate", children: [
4570
+ successRate,
4571
+ "%"
4572
+ ] }) }),
4573
+ /* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
4574
+ /* @__PURE__ */ jsx("span", { id: "successful-requests", children: metrics.successfulRequests }),
4575
+ " successful"
4576
+ ] })
4577
+ ] }),
4578
+ /* @__PURE__ */ jsxs("div", { class: "card", children: [
4579
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Fail Rate" }),
4580
+ /* @__PURE__ */ jsx("div", { class: "card-value text-error", children: /* @__PURE__ */ jsxs("span", { id: "fail-rate", children: [
4581
+ failRate,
4582
+ "%"
4583
+ ] }) }),
4584
+ /* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
4585
+ /* @__PURE__ */ jsx("span", { id: "failed-requests", children: metrics.failedRequests }),
4586
+ " failed"
4587
+ ] })
4588
+ ] }),
4589
+ /* @__PURE__ */ jsxs("div", { class: "card", children: [
4590
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Avg Latency" }),
4591
+ /* @__PURE__ */ jsxs("div", { class: "card-value", children: [
4592
+ /* @__PURE__ */ jsx("span", { id: "avg-latency", children: metrics.averageTotalTime_ms.toFixed(2) }),
4593
+ " ",
4594
+ /* @__PURE__ */ jsx("span", { style: "font-size: 1rem; color: var(--text-secondary)", children: "ms" })
4595
+ ] })
4596
+ ] })
4597
+ ] });
4598
+ }
4599
+ function ChartCard({ title, id }) {
4600
+ return /* @__PURE__ */ jsxs("div", { class: "card", style: "height: 300px;", children: [
4601
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: title }),
4602
+ /* @__PURE__ */ jsx("div", { class: "card-chart", children: /* @__PURE__ */ jsx("canvas", { id }) })
4603
+ ] });
4604
+ }
4605
+ function Card({ title, contentId }) {
4606
+ return /* @__PURE__ */ jsxs("div", { class: "card", children: [
4607
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: title }),
4608
+ /* @__PURE__ */ jsx("div", { id: contentId })
4609
+ ] });
4610
+ }
3586
4611
  const INTERVALS = [
3587
4612
  { label: "10s", ms: 10 * 1e3 },
3588
4613
  { label: "1m", ms: 60 * 1e3 },
@@ -3597,19 +4622,19 @@ const INTERVALS = [
3597
4622
  { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
3598
4623
  ];
3599
4624
  class MetricsCollector {
3600
- currentIntervalStart = {};
3601
- pendingDetails = {};
3602
- eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
3603
- timer = null;
3604
- constructor() {
4625
+ constructor(db) {
4626
+ this.db = db;
3605
4627
  this.eventLoopHistogram.enable();
3606
4628
  const now = Date.now();
3607
4629
  INTERVALS.forEach((int) => {
3608
4630
  this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3609
4631
  this.pendingDetails[int.label] = [];
3610
4632
  });
3611
- this.timer = setInterval(() => this.collect(), 1e4);
3612
4633
  }
4634
+ currentIntervalStart = {};
4635
+ pendingDetails = {};
4636
+ eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
4637
+ timer = null;
3613
4638
  recordRequest(duration, isError) {
3614
4639
  INTERVALS.forEach((int) => {
3615
4640
  this.pendingDetails[int.label].push({ duration, isError });
@@ -3621,11 +4646,9 @@ class MetricsCollector {
3621
4646
  async collect() {
3622
4647
  try {
3623
4648
  const now = Date.now();
3624
- console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
3625
4649
  for (const int of INTERVALS) {
3626
4650
  const start = this.currentIntervalStart[int.label];
3627
4651
  if (now >= start + int.ms) {
3628
- console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
3629
4652
  await this.flushInterval(int.label, start, int.ms);
3630
4653
  this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3631
4654
  }
@@ -3636,10 +4659,8 @@ class MetricsCollector {
3636
4659
  }
3637
4660
  async flushInterval(label, timestamp, durationMs) {
3638
4661
  const reqs = this.pendingDetails[label];
3639
- console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
3640
4662
  this.pendingDetails[label] = [];
3641
4663
  if (reqs.length === 0) {
3642
- console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
3643
4664
  return;
3644
4665
  }
3645
4666
  const totalReqs = reqs.length;
@@ -3689,15 +4710,11 @@ class MetricsCollector {
3689
4710
  p99: getP(0.99)
3690
4711
  }
3691
4712
  };
3692
- console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
3693
4713
  try {
3694
4714
  const recordId = new RecordId("metrics", timestamp);
3695
- await datastore.set(recordId, metric);
3696
- console.log(`[MetricsCollector] Successfully saved ${label} metric to datastore`);
3697
- const test = await datastore.get(recordId);
3698
- console.log(`[MetricsCollector] DEBUG: Immediate .get() returned:`, test ? "DATA" : "NULL");
3699
- const queryTest = await datastore.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
3700
- console.log(`[MetricsCollector] DEBUG: Query by id returned ${queryTest[0]?.length || 0} records`);
4715
+ await this.db.upsert(recordId, metric);
4716
+ const test = await this.db.select(recordId);
4717
+ const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
3701
4718
  } catch (e) {
3702
4719
  console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
3703
4720
  }
@@ -3732,16 +4749,8 @@ class Dashboard {
3732
4749
  constructor(dashboardConfig = {}) {
3733
4750
  this.dashboardConfig = dashboardConfig;
3734
4751
  }
3735
- static __dirname = dirname(fileURLToPath(import.meta.url));
3736
- // Get base path for dashboard files - works in both dev (src/) and production (dist/)
3737
- static getBasePath() {
3738
- const dir = dirname(fileURLToPath(import.meta.url));
3739
- if (dir.endsWith("dist")) {
3740
- return dir + "/plugins/application/dashboard";
3741
- }
3742
- return dir;
3743
- }
3744
- router = new ShokupanRouter();
4752
+ [$appRoot];
4753
+ router = new ShokupanRouter({ renderer: renderToString });
3745
4754
  metrics = {
3746
4755
  totalRequests: 0,
3747
4756
  successfulRequests: 0,
@@ -3754,16 +4763,16 @@ class Dashboard {
3754
4763
  nodeMetrics: {},
3755
4764
  edgeMetrics: {}
3756
4765
  };
3757
- eta = new Eta({
3758
- views: Dashboard.getBasePath() + "/static",
3759
- cache: false
3760
- });
3761
4766
  startTime = Date.now();
3762
4767
  instrumented = false;
3763
- metricsCollector = new MetricsCollector();
4768
+ metricsCollector;
4769
+ get db() {
4770
+ return this[$appRoot].db;
4771
+ }
3764
4772
  // ShokupanPlugin interface implementation
3765
4773
  onInit(app, options) {
3766
4774
  this[$appRoot] = app;
4775
+ this.metricsCollector = new MetricsCollector(this.db);
3767
4776
  const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
3768
4777
  const hooks = this.getHooks();
3769
4778
  if (!app.middleware) {
@@ -3783,6 +4792,47 @@ class Dashboard {
3783
4792
  app.mount(mountPath, this.router);
3784
4793
  this.setupRoutes();
3785
4794
  }
4795
+ detectIntegrations() {
4796
+ const integrations = {};
4797
+ const routers = this[$appRoot]?.[$childRouters] || [];
4798
+ const checkConfig = (key) => {
4799
+ const conf = this.dashboardConfig.integrations?.[key];
4800
+ if (conf === false) return { enabled: false };
4801
+ if (typeof conf === "object" && conf.path) return { enabled: true, path: conf.path };
4802
+ return { enabled: true };
4803
+ };
4804
+ const scalarConf = checkConfig("scalar");
4805
+ if (scalarConf.enabled) {
4806
+ if (scalarConf.path) {
4807
+ integrations["scalar"] = scalarConf.path;
4808
+ } else {
4809
+ const plugin = routers.find((r) => r.constructor.name === "ScalarPlugin");
4810
+ if (plugin) {
4811
+ integrations["scalar"] = plugin[$mountPath];
4812
+ }
4813
+ }
4814
+ }
4815
+ const asyncApiConf = checkConfig("asyncapi");
4816
+ if (asyncApiConf.enabled) {
4817
+ if (asyncApiConf.path) {
4818
+ integrations["asyncapi"] = asyncApiConf.path;
4819
+ } else {
4820
+ const plugin = routers.find((r) => r.constructor.name === "AsyncApiPlugin");
4821
+ if (plugin) {
4822
+ integrations["asyncapi"] = plugin[$mountPath];
4823
+ }
4824
+ }
4825
+ }
4826
+ return integrations;
4827
+ }
4828
+ // Get base path for dashboard files - works in both dev (src/) and production (dist/)
4829
+ static getBasePath() {
4830
+ const dir = dirname(fileURLToPath(import.meta.url));
4831
+ if (dir.endsWith("dist")) {
4832
+ return dir + "/plugins/application/dashboard";
4833
+ }
4834
+ return dir;
4835
+ }
3786
4836
  setupRoutes() {
3787
4837
  this.router.get("/metrics", async (ctx) => {
3788
4838
  const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
@@ -3807,7 +4857,7 @@ class Dashboard {
3807
4857
  const startTime = Date.now() - ms;
3808
4858
  let stats;
3809
4859
  try {
3810
- stats = await datastore.query(`
4860
+ stats = await this.db.query(`
3811
4861
  SELECT
3812
4862
  count() as total,
3813
4863
  count(IF status < 400 THEN 1 END) as success,
@@ -3868,7 +4918,7 @@ class Dashboard {
3868
4918
  const periodMs = intervalMap[interval] || 60 * 1e3;
3869
4919
  const startTime = Date.now() - periodMs * 3;
3870
4920
  const endTime = Date.now();
3871
- const result = await datastore.query(
4921
+ const result = await this.db.query(
3872
4922
  "SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
3873
4923
  { start: startTime, end: endTime, interval }
3874
4924
  );
@@ -3897,7 +4947,7 @@ class Dashboard {
3897
4947
  };
3898
4948
  this.router.get("/requests/top", async (ctx) => {
3899
4949
  const startTime = getIntervalStartTime(ctx.query["interval"]);
3900
- const result = await datastore.query(
4950
+ const result = await this.db.query(
3901
4951
  "SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3902
4952
  { start: startTime }
3903
4953
  );
@@ -3905,7 +4955,7 @@ class Dashboard {
3905
4955
  });
3906
4956
  this.router.get("/errors/top", async (ctx) => {
3907
4957
  const startTime = getIntervalStartTime(ctx.query["interval"]);
3908
- const result = await datastore.query(
4958
+ const result = await this.db.query(
3909
4959
  "SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
3910
4960
  { start: startTime }
3911
4961
  );
@@ -3913,7 +4963,7 @@ class Dashboard {
3913
4963
  });
3914
4964
  this.router.get("/requests/failing", async (ctx) => {
3915
4965
  const startTime = getIntervalStartTime(ctx.query["interval"]);
3916
- const result = await datastore.query(
4966
+ const result = await this.db.query(
3917
4967
  "SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3918
4968
  { start: startTime }
3919
4969
  );
@@ -3921,7 +4971,7 @@ class Dashboard {
3921
4971
  });
3922
4972
  this.router.get("/requests/slowest", async (ctx) => {
3923
4973
  const startTime = getIntervalStartTime(ctx.query["interval"]);
3924
- const result = await datastore.query(
4974
+ const result = await this.db.query(
3925
4975
  "SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
3926
4976
  { start: startTime }
3927
4977
  );
@@ -3939,15 +4989,15 @@ class Dashboard {
3939
4989
  return ctx.json({ registry: registry || {} });
3940
4990
  });
3941
4991
  this.router.get("/requests", async (ctx) => {
3942
- const result = await datastore.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
4992
+ const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
3943
4993
  return ctx.json({ requests: result[0] || [] });
3944
4994
  });
3945
4995
  this.router.get("/requests/:id", async (ctx) => {
3946
- const result = await datastore.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
4996
+ const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
3947
4997
  return ctx.json({ request: result[0]?.[0] });
3948
4998
  });
3949
4999
  this.router.get("/failures", async (ctx) => {
3950
- const result = await datastore.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
5000
+ const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
3951
5001
  return ctx.json({ failures: result[0] });
3952
5002
  });
3953
5003
  this.router.post("/replay", async (ctx) => {
@@ -3971,18 +5021,51 @@ class Dashboard {
3971
5021
  return ctx.json({ error: String(e) }, 500);
3972
5022
  }
3973
5023
  });
3974
- this.router.get("/", async (ctx) => {
5024
+ this.router.get("/**", async (ctx) => {
5025
+ const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
5026
+ let relativePath = ctx.path;
5027
+ if (relativePath.startsWith(mountPath)) {
5028
+ relativePath = relativePath.slice(mountPath.length);
5029
+ }
5030
+ if (relativePath.startsWith("/")) {
5031
+ relativePath = relativePath.slice(1);
5032
+ }
5033
+ const path = relativePath;
5034
+ const staticFiles = [
5035
+ "charts.js",
5036
+ "failures.js",
5037
+ "graph.mjs",
5038
+ "poll.js",
5039
+ "reactflow.css",
5040
+ "registry.css",
5041
+ "registry.js",
5042
+ "requests.js",
5043
+ "styles.css",
5044
+ "tables.js",
5045
+ "tabs.js",
5046
+ "tabulator.css",
5047
+ "theme.css"
5048
+ ];
5049
+ if (staticFiles.includes(path)) {
5050
+ const content = await readFile(join$1(Dashboard.getBasePath(), "static", path), "utf-8");
5051
+ if (path.endsWith(".css")) ctx.set("Content-Type", "text/css");
5052
+ else if (path.endsWith(".js") || path.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
5053
+ return ctx.send(content);
5054
+ }
3975
5055
  const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
3976
5056
  const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
3977
- const linkPattern = this.getLinkPattern();
3978
- const template = await readFile(Dashboard.getBasePath() + "/template.eta", "utf8");
3979
- return ctx.html(this.eta.renderString(template, {
5057
+ this.getLinkPattern();
5058
+ const integrations = this.detectIntegrations();
5059
+ const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
5060
+ const html = renderToString(DashboardApp({
3980
5061
  metrics: this.metrics,
3981
5062
  uptime,
3982
5063
  rootPath: process.cwd(),
3983
- linkPattern,
3984
- headers: this.dashboardConfig.getRequestHeaders?.()
5064
+ integrations,
5065
+ base: mountPath,
5066
+ getRequestHeadersSource
3985
5067
  }));
5068
+ return ctx.html(`<!DOCTYPE html>${html}`);
3986
5069
  });
3987
5070
  }
3988
5071
  instrumentApp(app) {
@@ -4078,7 +5161,7 @@ class Dashboard {
4078
5161
  headers[k] = v;
4079
5162
  });
4080
5163
  }
4081
- await datastore.set(new RecordId("failed_requests", ctx.requestId), {
5164
+ await this.db.upsert(new RecordId("failed_requests", ctx.requestId), {
4082
5165
  method: ctx.method,
4083
5166
  url: ctx.url.toString(),
4084
5167
  headers,
@@ -4103,7 +5186,7 @@ class Dashboard {
4103
5186
  };
4104
5187
  this.metrics.logs.push(logEntry);
4105
5188
  try {
4106
- await datastore.set(new RecordId("requests", ctx.requestId), logEntry);
5189
+ await this.db.upsert(new RecordId("requests", ctx.requestId), logEntry);
4107
5190
  } catch (e) {
4108
5191
  console.error("Failed to record request log", e);
4109
5192
  }
@@ -4131,32 +5214,169 @@ class Dashboard {
4131
5214
  function unknownError(ctx) {
4132
5215
  return ctx.json({ error: "Unknown Error" }, 500);
4133
5216
  }
4134
- const eta = new Eta();
5217
+ class GraphQLApolloPlugin extends ShokupanRouter {
5218
+ // Use generic any or verify type
5219
+ constructor(pluginOptions) {
5220
+ super();
5221
+ this.pluginOptions = pluginOptions;
5222
+ this.pluginOptions.path ??= "/graphql";
5223
+ }
5224
+ apolloServer;
5225
+ async onInit(app, options) {
5226
+ const { ApolloServer, HeaderMap } = await import("@apollo/server");
5227
+ this.apolloServer = new ApolloServer({
5228
+ typeDefs: this.pluginOptions.typeDefs,
5229
+ resolvers: this.pluginOptions.resolvers,
5230
+ ...this.pluginOptions.apolloConfig || {}
5231
+ });
5232
+ const path = options?.path || this.pluginOptions.path || "/graphql";
5233
+ app.mount(path, this);
5234
+ app.onStart(async () => {
5235
+ await this.apolloServer.start();
5236
+ });
5237
+ this.post("/", async (ctx) => {
5238
+ const body = await ctx.body();
5239
+ const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
5240
+ httpGraphQLRequest: {
5241
+ body,
5242
+ method: ctx.req.method,
5243
+ search: ctx.url.search,
5244
+ headers: new HeaderMap(ctx.req.headers)
5245
+ },
5246
+ // Pass the Shokupan Context as the GraphQL Context
5247
+ context: async () => ({ ...ctx, shokupan: ctx })
5248
+ });
5249
+ for (const [key, value] of httpGraphQLResponse.headers) {
5250
+ ctx.set(key, value);
5251
+ }
5252
+ if (httpGraphQLResponse.body.kind === "complete") {
5253
+ return ctx.send(httpGraphQLResponse.body.string, {
5254
+ status: httpGraphQLResponse.status ?? 200
5255
+ });
5256
+ } else {
5257
+ let string = "";
5258
+ for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
5259
+ string += chunk;
5260
+ }
5261
+ return ctx.send(string, {
5262
+ status: httpGraphQLResponse.status ?? 200
5263
+ });
5264
+ }
5265
+ });
5266
+ this.get("/", async (ctx) => {
5267
+ const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
5268
+ httpGraphQLRequest: {
5269
+ body: Object.keys(ctx.query).length > 0 ? ctx.query : void 0,
5270
+ method: ctx.req.method,
5271
+ search: ctx.url.search,
5272
+ headers: new HeaderMap(ctx.req.headers)
5273
+ },
5274
+ context: async () => ({ ...ctx, shokupan: ctx })
5275
+ });
5276
+ for (const [key, value] of httpGraphQLResponse.headers) {
5277
+ ctx.set(key, value);
5278
+ }
5279
+ if (httpGraphQLResponse.body.kind === "complete") {
5280
+ return ctx.html(httpGraphQLResponse.body.string, httpGraphQLResponse.status ?? 200);
5281
+ } else {
5282
+ let string = "";
5283
+ for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
5284
+ string += chunk;
5285
+ }
5286
+ return ctx.html(string, httpGraphQLResponse.status ?? 200);
5287
+ }
5288
+ });
5289
+ }
5290
+ }
4135
5291
  class ScalarPlugin extends ShokupanRouter {
4136
5292
  constructor(pluginOptions = {}) {
4137
5293
  pluginOptions.config ??= {};
4138
5294
  super();
4139
5295
  this.pluginOptions = pluginOptions;
4140
- this.init();
5296
+ this.initRoutes();
5297
+ }
5298
+ eta;
5299
+ async onInit(app, options) {
5300
+ const { Eta: Eta2 } = await import("eta");
5301
+ this.eta = new Eta2();
5302
+ const path = options?.path || this.pluginOptions.path || "/reference";
5303
+ app.mount(path, this);
5304
+ this.onMount(app);
4141
5305
  }
4142
- onInit(app, options) {
4143
- if (options?.path) {
4144
- app.mount(options.path, this);
4145
- } else {
4146
- app.mount(options.path ?? "/", this);
5306
+ async ensureEta() {
5307
+ if (!this.eta) {
5308
+ const { Eta: Eta2 } = await import("eta");
5309
+ this.eta = new Eta2();
4147
5310
  }
4148
- this.onMount(app);
4149
5311
  }
4150
- init() {
4151
- this.get("/", (ctx) => {
5312
+ initRoutes() {
5313
+ const bootId = Date.now().toString();
5314
+ this.get("/_lifecycle", (ctx) => ctx.json({ boot: bootId }));
5315
+ this.get("/", async (ctx) => {
5316
+ await this.ensureEta();
4152
5317
  let path = ctx.url.toString();
4153
5318
  if (!path.endsWith("/")) path += "/";
4154
- return ctx.html(eta.renderString(`<!doctype html>
4155
- <html>
5319
+ const devScript = ctx.app?.applicationConfig.development ? `
5320
+ <script>
5321
+ (function() {
5322
+ const bootId = "${bootId}";
5323
+ let isDown = false;
5324
+
5325
+ setInterval(async () => {
5326
+ try {
5327
+ const res = await fetch('${path}_lifecycle');
5328
+ if (!res.ok) throw new Error('Down');
5329
+ const data = await res.json();
5330
+ if (data.boot !== bootId) {
5331
+ console.log('Server restarted, reloading...');
5332
+ window.location.reload();
5333
+ }
5334
+ else if (isDown) {
5335
+ isDown = false;
5336
+ }
5337
+ } catch (e) {
5338
+ isDown = true;
5339
+ console.log('Connection lost...');
5340
+ }
5341
+ }, 2000);
5342
+ })();
5343
+ <\/script>
5344
+ ` : "";
5345
+ let themeCss = "";
5346
+ try {
5347
+ try {
5348
+ themeCss = readFileSync(join$1(process.cwd(), "src/theme.css"), "utf-8");
5349
+ } catch {
5350
+ }
5351
+ } catch (e) {
5352
+ }
5353
+ if (!this.eta) throw new Error("Eta not initialized");
5354
+ return ctx.html(this.eta.renderString(`<!doctype html>
5355
+ <html lang="en">
4156
5356
  <head>
4157
5357
  <title>API Reference</title>
4158
5358
  <meta charset = "utf-8" />
4159
5359
  <meta name="viewport" content = "width=device-width, initial-scale=1" />
5360
+ <style>
5361
+ ${themeCss}
5362
+
5363
+ :root {
5364
+ --scalar-color-1: var(--primary);
5365
+ --scalar-color-2: var(--secondary);
5366
+ --scalar-color-3: var(--accent);
5367
+ --scalar-color-accent: var(--accent);
5368
+
5369
+ --scalar-background-1: var(--bg-primary);
5370
+ --scalar-background-2: var(--bg-secondary);
5371
+ --scalar-background-3: var(--bg-card);
5372
+
5373
+ --scalar-text-1: var(--text-primary);
5374
+ --scalar-text-2: var(--text-secondary);
5375
+ --scalar-text-3: var(--text-muted);
5376
+
5377
+ --scalar-border-color: var(--border-color);
5378
+ }
5379
+ </style>
4160
5380
  </head>
4161
5381
 
4162
5382
  <body>
@@ -4168,9 +5388,10 @@ class ScalarPlugin extends ShokupanRouter {
4168
5388
  }
4169
5389
  ])
4170
5390
  <\/script>
5391
+ <%~ it.devScript %>
4171
5392
  </body>
4172
5393
 
4173
- </html>`, { path, config: this.pluginOptions }));
5394
+ </html>`, { path, config: this.pluginOptions, devScript }));
4174
5395
  });
4175
5396
  this.get("/openapi.json", async (ctx) => {
4176
5397
  let spec;
@@ -4373,7 +5594,7 @@ function Cors(options = {}) {
4373
5594
  }
4374
5595
  const response = await next();
4375
5596
  if (response instanceof Response) {
4376
- const headerEntries = Array.from(headers.entries());
5597
+ const headerEntries = Object.entries(headers);
4377
5598
  for (let i = 0; i < headerEntries.length; i++) {
4378
5599
  const [key, value] = headerEntries[i];
4379
5600
  response.headers.set(key, value);
@@ -5145,6 +6366,7 @@ export {
5145
6366
  $url,
5146
6367
  $ws,
5147
6368
  All,
6369
+ AsyncApiPlugin,
5148
6370
  AuthPlugin,
5149
6371
  Body,
5150
6372
  ClusterPlugin,
@@ -5157,6 +6379,7 @@ export {
5157
6379
  Delete,
5158
6380
  Event,
5159
6381
  Get,
6382
+ GraphQLApolloPlugin,
5160
6383
  HTTPMethods,
5161
6384
  Head,
5162
6385
  Headers$1 as Headers,
@@ -5173,13 +6396,16 @@ export {
5173
6396
  RateLimitMiddleware,
5174
6397
  Req,
5175
6398
  RouteParamType,
6399
+ RouterRegistry,
5176
6400
  ScalarPlugin,
5177
6401
  SecurityHeaders,
5178
6402
  Session,
5179
6403
  Shokupan,
6404
+ ShokupanApplicationTree,
5180
6405
  ShokupanContext,
5181
6406
  ShokupanRequest,
5182
6407
  ShokupanResponse,
6408
+ ShokupanRouter,
5183
6409
  Spec,
5184
6410
  Use,
5185
6411
  ValidationError,