shokupan 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1459 -239
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +1441 -220
  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 +33 -13
  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.cjs CHANGED
@@ -24,21 +24,24 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
25
25
  const nanoid = require("nanoid");
26
26
  const promises = require("node:fs/promises");
27
+ const node_util = require("node:util");
27
28
  const api = require("@opentelemetry/api");
29
+ const jsYaml = require("js-yaml");
28
30
  const node_async_hooks = require("node:async_hooks");
29
31
  const surrealdb = require("surrealdb");
30
- const eta$2 = require("eta");
32
+ const eta$1 = require("eta");
31
33
  const promises$1 = require("fs/promises");
32
34
  const path = require("path");
33
35
  const os = require("node:os");
34
- const arctic = require("arctic");
35
- const jose = require("jose");
36
- const cluster = require("node:cluster");
37
- const net = require("node:net");
38
36
  const path$1 = require("node:path");
39
37
  const node_url = require("node:url");
38
+ const renderToString = require("preact-render-to-string");
39
+ const jsxRuntime = require("preact/jsx-runtime");
40
+ const cluster = require("node:cluster");
41
+ const net = require("node:net");
40
42
  const node_perf_hooks = require("node:perf_hooks");
41
- const analyzer = require("./analyzer-Bei1sVWp.cjs");
43
+ const fs = require("node:fs");
44
+ const analyzer = require("./analyzer-CKLGLFtx.cjs");
42
45
  const zlib = require("node:zlib");
43
46
  const Ajv = require("ajv");
44
47
  const addFormats = require("ajv-formats");
@@ -62,7 +65,6 @@ function _interopNamespaceDefault(e) {
62
65
  return Object.freeze(n);
63
66
  }
64
67
  const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
65
- const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
66
68
  const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
67
69
  const HTTP_STATUS = {
68
70
  // 2xx Success
@@ -311,6 +313,21 @@ class ShokupanContext {
311
313
  [$cachedHost];
312
314
  [$cachedOrigin];
313
315
  [$cachedQuery];
316
+ disconnectCallbacks = [];
317
+ /**
318
+ * Registers a callback to be executed when the associated WebSocket disconnects.
319
+ * This is only applicable for requests that are part of a WebSocket interaction or upgrade.
320
+ */
321
+ onSocketDisconnect(callback) {
322
+ this.disconnectCallbacks.push(callback);
323
+ }
324
+ /**
325
+ * @internal
326
+ * Retrieves registered disconnect callbacks for execution.
327
+ */
328
+ getDisconnectCallbacks() {
329
+ return this.disconnectCallbacks;
330
+ }
314
331
  [$ws];
315
332
  [$socket];
316
333
  [$io];
@@ -325,6 +342,20 @@ class ShokupanContext {
325
342
  get requestId() {
326
343
  return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid.nanoid();
327
344
  }
345
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
346
+ const innerString = node_util.inspect({
347
+ method: this.request.method,
348
+ url: this.request.url,
349
+ requestHeaders: new Map(this.request.headers),
350
+ sessionId: this.sessionID,
351
+ state: this.state,
352
+ params: this.params,
353
+ response: this[$finalResponse]?.body,
354
+ responseHeaders: new Map(this[$finalResponse]?.headers),
355
+ handlerStack: this.handlerStack.map((h) => h.name === "anonymous" ? h.file + ":" + h.line : h.name)
356
+ }, { depth: null, colors: true, numericSeparator: true, customInspect: true });
357
+ return "Context(" + this.requestId + ") {" + innerString.slice(1, -2) + ",\n ...others\n}";
358
+ }
328
359
  get url() {
329
360
  if (!this[$url]) {
330
361
  const urlString = this.request.url || "http://localhost/";
@@ -376,10 +407,8 @@ class ShokupanContext {
376
407
  if (this[$cachedQuery]) return this[$cachedQuery];
377
408
  const q = /* @__PURE__ */ Object.create(null);
378
409
  const blocklist = ["__proto__", "constructor", "prototype"];
379
- const entries = Object.entries(this.url.searchParams);
380
- for (let i = 0; i < entries.length; i++) {
381
- const [key, value] = entries[i];
382
- if (blocklist.includes(key)) continue;
410
+ this.url.searchParams.forEach((value, key) => {
411
+ if (blocklist.includes(key)) return;
383
412
  if (Object.prototype.hasOwnProperty.call(q, key)) {
384
413
  if (Array.isArray(q[key])) {
385
414
  q[key].push(value);
@@ -389,7 +418,7 @@ class ShokupanContext {
389
418
  } else {
390
419
  q[key] = value;
391
420
  }
392
- }
421
+ });
393
422
  this[$cachedQuery] = q;
394
423
  return q;
395
424
  }
@@ -682,12 +711,12 @@ class ShokupanContext {
682
711
  /**
683
712
  * Respond with a JSON object
684
713
  */
685
- json(data, status, headers) {
714
+ async json(data, status, headers) {
686
715
  const finalStatus = status ?? this.response.status ?? 200;
687
716
  if (!VALID_HTTP_STATUSES.has(finalStatus)) {
688
717
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
689
718
  }
690
- const jsonString = JSON.stringify(data);
719
+ const jsonString = JSON.stringify(data instanceof Promise ? await data : data);
691
720
  this[$rawBody] = jsonString;
692
721
  if (!headers && !this.response.hasPopulatedHeaders) {
693
722
  this[$finalResponse] = new Response(jsonString, {
@@ -704,14 +733,14 @@ class ShokupanContext {
704
733
  /**
705
734
  * Respond with a text string
706
735
  */
707
- text(data, status, headers) {
736
+ async text(data, status, headers) {
708
737
  const finalStatus = status ?? this.response.status ?? 200;
709
738
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
710
739
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
711
740
  }
712
- this[$rawBody] = data;
741
+ this[$rawBody] = data instanceof Promise ? await data : data;
713
742
  if (!headers && !this.response.hasPopulatedHeaders) {
714
- this[$finalResponse] = new Response(data, {
743
+ this[$finalResponse] = new Response(this[$rawBody], {
715
744
  status: finalStatus,
716
745
  headers: { "content-type": "text/plain; charset=utf-8" }
717
746
  });
@@ -719,71 +748,73 @@ class ShokupanContext {
719
748
  }
720
749
  const finalHeaders = this.mergeHeaders(headers);
721
750
  finalHeaders.set("content-type", "text/plain; charset=utf-8");
722
- this[$finalResponse] = new Response(data, { status: finalStatus, headers: finalHeaders });
751
+ this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
723
752
  return this[$finalResponse];
724
753
  }
725
754
  /**
726
755
  * Respond with HTML content
727
756
  */
728
- html(html, status, headers) {
757
+ async html(html, status, headers) {
729
758
  const finalStatus = status ?? this.response.status ?? 200;
730
759
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
731
760
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
732
761
  }
733
762
  const finalHeaders = this.mergeHeaders(headers);
734
763
  finalHeaders.set("content-type", "text/html; charset=utf-8");
735
- this[$rawBody] = html;
736
- this[$finalResponse] = new Response(html, { status: finalStatus, headers: finalHeaders });
764
+ this[$rawBody] = html instanceof Promise ? await html : html;
765
+ this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
737
766
  return this[$finalResponse];
738
767
  }
739
768
  /**
740
769
  * Respond with a redirect
741
770
  */
742
- redirect(url, status = 302) {
771
+ async redirect(url, status = 302) {
743
772
  if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
744
773
  throw new Error(`Invalid redirect status code: ${status}`);
745
774
  }
746
- const headers = this.mergeHeaders();
747
- headers.set("Location", url);
748
- this[$finalResponse] = new Response(null, { status, headers });
775
+ const finalHeaders = this.mergeHeaders();
776
+ finalHeaders.set("Location", url instanceof Promise ? await url : url);
777
+ this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
749
778
  return this[$finalResponse];
750
779
  }
751
780
  /**
752
781
  * Respond with a status code
753
782
  * DOES NOT CHAIN!
754
783
  */
755
- status(status) {
784
+ async status(statusCode) {
785
+ const status = statusCode instanceof Promise ? await statusCode : statusCode;
756
786
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
757
787
  throw new Error(`Invalid HTTP status code: ${status}`);
758
788
  }
759
- const headers = this.mergeHeaders();
760
- this[$finalResponse] = new Response(null, { status, headers });
789
+ const finalHeaders = this.mergeHeaders();
790
+ this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
761
791
  return this[$finalResponse];
762
792
  }
763
793
  /**
764
794
  * Respond with a file
765
795
  */
766
796
  async file(path2, fileOptions, responseOptions) {
767
- const headers = this.mergeHeaders(responseOptions?.headers);
797
+ const finalHeaders = this.mergeHeaders(responseOptions?.headers);
768
798
  const status = responseOptions?.status ?? this.response.status;
769
799
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
770
800
  throw new Error(`Invalid HTTP status code: ${status}`);
771
801
  }
772
802
  if (typeof Bun !== "undefined") {
773
- this[$finalResponse] = new Response(Bun.file(path2, fileOptions), { status, headers });
803
+ this[$finalResponse] = new Response(Bun.file(path2, fileOptions), { status, headers: finalHeaders });
774
804
  return this[$finalResponse];
775
805
  } else {
776
806
  const fileBuffer = await promises.readFile(path2);
777
807
  if (fileOptions?.type) {
778
- headers.set("content-type", fileOptions.type);
808
+ finalHeaders.set("content-type", fileOptions.type);
779
809
  }
780
- this[$finalResponse] = new Response(fileBuffer, { status, headers });
810
+ this[$finalResponse] = new Response(fileBuffer, { status, headers: finalHeaders });
781
811
  return this[$finalResponse];
782
812
  }
783
813
  }
784
814
  /**
785
815
  * Render a JSX element
786
816
  * @param element JSX Element
817
+ * @param args JSX Element Args/Props
787
818
  * @param status HTTP Status
788
819
  * @param headers HTTP Headers
789
820
  */
@@ -837,29 +868,6 @@ const compose = (middleware) => {
837
868
  return runner(0);
838
869
  };
839
870
  };
840
- const tracer = api.trace.getTracer("shokupan.middleware");
841
- function traceHandler(fn, name) {
842
- return async function(...args) {
843
- return tracer.startActiveSpan(`route handler - ${name}`, {
844
- kind: api.SpanKind.INTERNAL,
845
- attributes: {
846
- "http.route": name,
847
- "component": "shokupan.route"
848
- }
849
- }, async (span) => {
850
- try {
851
- const result = await fn.apply(this, args);
852
- return result;
853
- } catch (err) {
854
- span.recordException(err);
855
- span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
856
- throw err;
857
- } finally {
858
- span.end();
859
- }
860
- });
861
- };
862
- }
863
871
  function isObject(item) {
864
872
  return item && typeof item === "object" && !Array.isArray(item);
865
873
  }
@@ -1032,24 +1040,34 @@ function analyzeHandler(handler) {
1032
1040
  }
1033
1041
  return { inferredSpec };
1034
1042
  }
1035
- async function getAstRoutes(applications) {
1043
+ async function getAstRoutes$1(applications) {
1036
1044
  const astRoutes = [];
1037
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
1045
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
1038
1046
  if (seen.has(app.name)) return [];
1039
1047
  const newSeen = new Set(seen);
1040
1048
  newSeen.add(app.name);
1041
1049
  const expanded = [];
1050
+ let currentPrefix = prefix;
1051
+ if (app.controllerPrefix) {
1052
+ const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
1053
+ const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
1054
+ currentPrefix = cleanPrefix + cleanCont;
1055
+ }
1042
1056
  for (const route of app.routes) {
1043
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1057
+ const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
1044
1058
  const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1045
1059
  let joined = cleanPrefix + cleanPath;
1046
1060
  if (joined.length > 1 && joined.endsWith("/")) {
1047
1061
  joined = joined.slice(0, -1);
1048
1062
  }
1049
- expanded.push({
1063
+ const expandedRoute = {
1050
1064
  ...route,
1051
1065
  path: joined || "/"
1052
- });
1066
+ };
1067
+ if (sourceOverride) {
1068
+ expandedRoute.sourceContext = sourceOverride;
1069
+ }
1070
+ expanded.push(expandedRoute);
1053
1071
  }
1054
1072
  if (app.mounted) {
1055
1073
  for (const mount of app.mounted) {
@@ -1057,7 +1075,23 @@ async function getAstRoutes(applications) {
1057
1075
  if (targetApp) {
1058
1076
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1059
1077
  const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
1060
- expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
1078
+ let nextSourceOverride = sourceOverride;
1079
+ if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
1080
+ if (mount.sourceContext) {
1081
+ nextSourceOverride = {
1082
+ ...mount.sourceContext,
1083
+ // Add highlight for the mount line to make it clear
1084
+ highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
1085
+ highlights: [{
1086
+ startLine: mount.sourceContext.startLine,
1087
+ endLine: mount.sourceContext.endLine,
1088
+ type: "return-success"
1089
+ // Use the success color (cyan) for the mount point
1090
+ }]
1091
+ };
1092
+ }
1093
+ }
1094
+ expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
1061
1095
  }
1062
1096
  }
1063
1097
  }
@@ -1085,13 +1119,13 @@ async function generateOpenApi(rootRouter, options = {}) {
1085
1119
  const defaultTagName = options.defaultTag || "Application";
1086
1120
  let astRoutes = [];
1087
1121
  try {
1088
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-Bei1sVWp.cjs"));
1122
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-CKLGLFtx.cjs"));
1089
1123
  const analyzer2 = new OpenAPIAnalyzer(process.cwd());
1090
1124
  const { applications } = await analyzer2.analyze();
1091
- astRoutes = await getAstRoutes(applications);
1125
+ astRoutes = await getAstRoutes$1(applications);
1092
1126
  } catch (e) {
1093
1127
  }
1094
- const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
1128
+ const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = []) => {
1095
1129
  let group = currentGroup;
1096
1130
  let tag = defaultTag;
1097
1131
  if (router.config?.group) group = router.config.group;
@@ -1108,21 +1142,33 @@ async function generateOpenApi(rootRouter, options = {}) {
1108
1142
  }
1109
1143
  }
1110
1144
  if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
1145
+ const routerMiddleware = router.middleware || [];
1111
1146
  const routes = router[$routes] || [];
1112
1147
  for (const route of routes) {
1148
+ if (!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"].includes(route.method.toUpperCase())) {
1149
+ continue;
1150
+ }
1113
1151
  const routeGroup = route.group || group;
1114
1152
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1115
1153
  const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1116
1154
  let fullPath = cleanPrefix + cleanSubPath || "/";
1117
- fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
1118
1155
  if (fullPath.length > 1 && fullPath.endsWith("/")) {
1119
1156
  fullPath = fullPath.slice(0, -1);
1120
1157
  }
1158
+ fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
1121
1159
  if (!paths[fullPath]) paths[fullPath] = {};
1122
1160
  const operation = {
1123
1161
  responses: { "200": { description: "Successful response" } },
1124
1162
  tags: [tag]
1125
1163
  };
1164
+ const routeMiddleware = route.middleware || [];
1165
+ const allMiddleware = [...inheritedMiddleware, ...routerMiddleware, ...routeMiddleware];
1166
+ if (allMiddleware.length > 0) {
1167
+ operation["x-shokupan-middleware"] = allMiddleware.map((mw) => ({
1168
+ name: mw.name || "middleware",
1169
+ metadata: mw.metadata
1170
+ }));
1171
+ }
1126
1172
  if (route.guards) {
1127
1173
  for (const guard of route.guards) {
1128
1174
  if (guard.spec) {
@@ -1160,6 +1206,23 @@ async function generateOpenApi(rootRouter, options = {}) {
1160
1206
  if (astMatch.description) operation.description = astMatch.description;
1161
1207
  if (astMatch.tags) operation.tags = astMatch.tags;
1162
1208
  if (astMatch.operationId) operation.operationId = astMatch.operationId;
1209
+ if (astMatch.sourceContext) {
1210
+ const sc = astMatch.sourceContext;
1211
+ operation["x-source-info"] = {
1212
+ file: sc.file,
1213
+ line: sc.startLine,
1214
+ snippet: sc.snippet || astMatch.handlerSource,
1215
+ // Fallback
1216
+ offset: sc.snippetStartLine || sc.startLine,
1217
+ highlightLines: [sc.startLine, sc.endLine],
1218
+ highlights: sc.highlights
1219
+ };
1220
+ operation["x-shokupan-source"] = {
1221
+ file: sc.file,
1222
+ line: sc.startLine,
1223
+ code: sc.snippet || astMatch.handlerSource || ""
1224
+ };
1225
+ }
1163
1226
  if (astMatch.requestTypes?.body) {
1164
1227
  operation.requestBody = {
1165
1228
  content: { "application/json": { schema: astMatch.requestTypes.body } }
@@ -1171,10 +1234,12 @@ async function generateOpenApi(rootRouter, options = {}) {
1171
1234
  content: { "application/json": { schema: astMatch.responseSchema } }
1172
1235
  };
1173
1236
  } else if (astMatch.responseType) {
1174
- const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
1237
+ let contentType = "application/json";
1238
+ if (astMatch.responseType === "string") contentType = "text/plain";
1239
+ else if (astMatch.responseType === "html") contentType = "text/html";
1175
1240
  operation.responses["200"] = {
1176
1241
  description: "Successful response",
1177
- content: { [contentType]: { schema: { type: astMatch.responseType } } }
1242
+ content: { [contentType]: { schema: { type: "string" } } }
1178
1243
  };
1179
1244
  }
1180
1245
  const params = [];
@@ -1186,6 +1251,26 @@ async function generateOpenApi(rootRouter, options = {}) {
1186
1251
  if (params.length > 0) {
1187
1252
  operation.parameters = params;
1188
1253
  }
1254
+ } else {
1255
+ const runtimeSource = (route.handler.originalHandler || route.handler).toString();
1256
+ let file;
1257
+ let line;
1258
+ if (route.metadata?.file) {
1259
+ file = route.metadata.file;
1260
+ line = route.metadata.line || 1;
1261
+ }
1262
+ operation["x-source-info"] = {
1263
+ snippet: runtimeSource,
1264
+ isRuntime: true,
1265
+ ...file ? { file, line: line || 1 } : {}
1266
+ };
1267
+ if (file) {
1268
+ operation["x-shokupan-source"] = {
1269
+ file,
1270
+ line: line || 1,
1271
+ code: runtimeSource
1272
+ };
1273
+ }
1189
1274
  }
1190
1275
  if (route.keys.length > 0) {
1191
1276
  const pathParams = route.keys.map((key) => ({
@@ -1255,7 +1340,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1255
1340
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1256
1341
  const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
1257
1342
  const nextPrefix = cleanPrefix + cleanMount || "/";
1258
- collect(child, nextPrefix, group, tag);
1343
+ collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware]);
1259
1344
  }
1260
1345
  };
1261
1346
  collect(rootRouter);
@@ -1308,7 +1393,7 @@ class EventError extends HttpError {
1308
1393
  this.name = "EventError";
1309
1394
  }
1310
1395
  }
1311
- const eta$1 = new eta$2.Eta();
1396
+ const eta = new eta$1.Eta();
1312
1397
  function serveStatic(config, prefix) {
1313
1398
  const rootPath = path.resolve(config.root || ".");
1314
1399
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
@@ -1407,7 +1492,7 @@ function serveStatic(config, prefix) {
1407
1492
  if (config.listDirectory) {
1408
1493
  try {
1409
1494
  const files = await promises$1.readdir(requestPath);
1410
- const listing = eta$1.renderString(`
1495
+ const listing = eta.renderString(`
1411
1496
  <!DOCTYPE html>
1412
1497
  <html>
1413
1498
  <head>
@@ -1447,7 +1532,7 @@ function serveStatic(config, prefix) {
1447
1532
  if (typeof Bun !== "undefined") {
1448
1533
  response = new Response(Bun.file(finalPath));
1449
1534
  } else {
1450
- const fileBuffer = await promises$1.readFile(finalPath);
1535
+ const fileBuffer = await promises$1.readFile(finalPath, { encoding: "binary" });
1451
1536
  response = new Response(fileBuffer);
1452
1537
  }
1453
1538
  if (config.hooks?.onResponse) {
@@ -1460,67 +1545,6 @@ function serveStatic(config, prefix) {
1460
1545
  serveStaticMiddleware.pluginName = "ServeStatic";
1461
1546
  return serveStaticMiddleware;
1462
1547
  }
1463
- const G = globalThis;
1464
- G.__shokupan_db = G.__shokupan_db || null;
1465
- G.__shokupan_db_promise = G.__shokupan_db_promise || null;
1466
- async function ensureDb() {
1467
- if (G.__shokupan_db) return G.__shokupan_db;
1468
- if (G.__shokupan_db_promise) return G.__shokupan_db_promise;
1469
- G.__shokupan_db_promise = (async () => {
1470
- try {
1471
- const { createNodeEngines } = await import("@surrealdb/node");
1472
- const surreal = await import("surrealdb");
1473
- const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1474
- const _db = new surrealdb.Surreal({
1475
- engines: createNodeEngines()
1476
- });
1477
- await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1478
- await _db.query(`
1479
- DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1480
- DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1481
- DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1482
- DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1483
- DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1484
- DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1485
- DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
1486
- `);
1487
- G.__shokupan_db = _db;
1488
- return _db;
1489
- } catch (e) {
1490
- G.__shokupan_db_promise = null;
1491
- if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1492
- throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1493
- }
1494
- throw e;
1495
- }
1496
- })();
1497
- return G.__shokupan_db_promise;
1498
- }
1499
- const datastore = {
1500
- async get(recordId) {
1501
- await ensureDb();
1502
- return G.__shokupan_db.select(recordId);
1503
- },
1504
- async set(recordId, value) {
1505
- await ensureDb();
1506
- return G.__shokupan_db.upsert(recordId).content(value);
1507
- },
1508
- async query(query, vars) {
1509
- await ensureDb();
1510
- try {
1511
- return G.__shokupan_db.query(query, vars).collect();
1512
- } catch (e) {
1513
- console.error("DS ERROR:", e);
1514
- throw e;
1515
- }
1516
- },
1517
- get ready() {
1518
- return ensureDb().then(() => void 0);
1519
- }
1520
- };
1521
- process.on("exit", async () => {
1522
- if (G.__shokupan_db) await G.__shokupan_db.close();
1523
- });
1524
1548
  class Container {
1525
1549
  static services = /* @__PURE__ */ new Map();
1526
1550
  static register(target, instance) {
@@ -1554,6 +1578,29 @@ function Inject(token) {
1554
1578
  });
1555
1579
  };
1556
1580
  }
1581
+ const tracer = api.trace.getTracer("shokupan.middleware");
1582
+ function traceHandler(fn, name) {
1583
+ return async function(...args) {
1584
+ return tracer.startActiveSpan(`route handler - ${name}`, {
1585
+ kind: api.SpanKind.INTERNAL,
1586
+ attributes: {
1587
+ "http.route": name,
1588
+ "component": "shokupan.route"
1589
+ }
1590
+ }, async (span) => {
1591
+ try {
1592
+ const result = await fn.apply(this, args);
1593
+ return result;
1594
+ } catch (err) {
1595
+ span.recordException(err);
1596
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
1597
+ throw err;
1598
+ } finally {
1599
+ span.end();
1600
+ }
1601
+ });
1602
+ };
1603
+ }
1557
1604
  class ShokupanRequestBase {
1558
1605
  method;
1559
1606
  url;
@@ -1591,8 +1638,10 @@ function getCallerInfo(skipFrames = 1) {
1591
1638
  if (!l.includes(":")) continue;
1592
1639
  if (l.includes("node_modules")) continue;
1593
1640
  if (l.includes("bun:main")) continue;
1641
+ if (l.includes("bun:wrap")) continue;
1594
1642
  if (l.includes("src/util/stack.ts")) continue;
1595
1643
  if (l.includes("src/router.ts")) continue;
1644
+ if (l.includes("src/util/decorators.ts")) continue;
1596
1645
  if (l.includes("src/shokupan.ts")) continue;
1597
1646
  found++;
1598
1647
  if (found >= skipFrames) {
@@ -1735,6 +1784,9 @@ class ShokupanRouter {
1735
1784
  [$parent] = null;
1736
1785
  [$childRouters] = [];
1737
1786
  [$childControllers] = [];
1787
+ get db() {
1788
+ return this.root?.db;
1789
+ }
1738
1790
  hookCache = /* @__PURE__ */ new Map();
1739
1791
  hooksInitialized = false;
1740
1792
  middleware = [];
@@ -1751,6 +1803,14 @@ class ShokupanRouter {
1751
1803
  // Metadata for the router itself
1752
1804
  currentGuards = [];
1753
1805
  eventHandlers = /* @__PURE__ */ new Map();
1806
+ /**
1807
+ * Registers middleware for this router.
1808
+ * Middleware will run for all routes matched by this router.
1809
+ */
1810
+ use(middleware) {
1811
+ this.middleware.push(middleware);
1812
+ return this;
1813
+ }
1754
1814
  // Registry Accessor
1755
1815
  getComponentRegistry() {
1756
1816
  const controllerRoutesMap = /* @__PURE__ */ new Map();
@@ -1815,6 +1875,8 @@ class ShokupanRouter {
1815
1875
  * Registers an event handler for WebSocket.
1816
1876
  */
1817
1877
  event(name, handler) {
1878
+ const info = getCallerInfo();
1879
+ handler.source = { file: info.file, line: info.line };
1818
1880
  if (this.eventHandlers.has(name)) {
1819
1881
  const err = new EventError(`Event handler \`${name}\` already exists.`);
1820
1882
  console.warn(err);
@@ -1839,6 +1901,12 @@ class ShokupanRouter {
1839
1901
  }
1840
1902
  return null;
1841
1903
  }
1904
+ /**
1905
+ * Returns all registered event handlers.
1906
+ */
1907
+ getEventHandlers() {
1908
+ return this.eventHandlers;
1909
+ }
1842
1910
  /**
1843
1911
  * Mounts a controller instance to a path prefix.
1844
1912
  *
@@ -2082,10 +2150,12 @@ class ShokupanRouter {
2082
2150
  if (typeof originalHandler !== "function") continue;
2083
2151
  let method;
2084
2152
  let subPath = "";
2153
+ let methodSource;
2085
2154
  if (decoratedRoutes && decoratedRoutes.has(name)) {
2086
2155
  const config = decoratedRoutes.get(name);
2087
2156
  method = config.method;
2088
2157
  subPath = config.path;
2158
+ methodSource = config.source;
2089
2159
  } else {
2090
2160
  for (let j = 0; j < HTTPMethods.length; j++) {
2091
2161
  const m = HTTPMethods[j];
@@ -2215,7 +2285,16 @@ class ShokupanRouter {
2215
2285
  const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2216
2286
  const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2217
2287
  const spec = { tags: [tagName], ...userSpec };
2218
- this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2288
+ this.add({
2289
+ method,
2290
+ path: normalizedPath,
2291
+ handler: finalHandler,
2292
+ spec,
2293
+ controller: instance,
2294
+ metadata: methodSource || instance.metadata,
2295
+ middleware: allMiddleware
2296
+ // Capture all resolved middleware
2297
+ });
2219
2298
  }
2220
2299
  if (decoratedEvents?.has(name)) {
2221
2300
  routesAttached++;
@@ -2248,6 +2327,11 @@ class ShokupanRouter {
2248
2327
  }
2249
2328
  return originalHandler.apply(instance, args);
2250
2329
  };
2330
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2331
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2332
+ const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
2333
+ wrappedHandler.spec = spec;
2334
+ wrappedHandler.originalHandler = originalHandler;
2251
2335
  this.event(config.eventName, wrappedHandler);
2252
2336
  }
2253
2337
  }
@@ -2317,7 +2401,7 @@ class ShokupanRouter {
2317
2401
  * @param arg.renderer - JSX renderer for the route
2318
2402
  * @param arg.controller - Controller for the route
2319
2403
  */
2320
- add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
2404
+ add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller, metadata, middleware }) {
2321
2405
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
2322
2406
  if (this.currentGuards.length > 0) {
2323
2407
  spec = spec || {};
@@ -2335,7 +2419,13 @@ class ShokupanRouter {
2335
2419
  }
2336
2420
  }
2337
2421
  }
2338
- let wrappedHandler = handler;
2422
+ let wrappedHandler = async (ctx) => {
2423
+ if (ctx.upgrade()) {
2424
+ return void 0;
2425
+ }
2426
+ return handler(ctx);
2427
+ };
2428
+ wrappedHandler.originalHandler = handler.originalHandler || handler;
2339
2429
  const routeGuards = [...this.currentGuards];
2340
2430
  const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
2341
2431
  if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
@@ -2386,7 +2476,7 @@ class ShokupanRouter {
2386
2476
  return innerHandler(ctx);
2387
2477
  };
2388
2478
  }
2389
- const { file, line } = getCallerInfo();
2479
+ const { file, line } = metadata || getCallerInfo();
2390
2480
  const trackingHandler = wrappedHandler;
2391
2481
  wrappedHandler = async (ctx) => {
2392
2482
  if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
@@ -2412,8 +2502,10 @@ class ShokupanRouter {
2412
2502
  const config = ctx.app.applicationConfig;
2413
2503
  Promise.resolve().then(async () => {
2414
2504
  try {
2505
+ const db = ctx.app?.db;
2506
+ if (!db) return;
2415
2507
  const timestamp = Date.now();
2416
- await datastore.set(new surrealdb.RecordId("middleware_tracking", {
2508
+ await db.upsert(new surrealdb.RecordId("middleware_tracking", {
2417
2509
  timestamp,
2418
2510
  name: handler.name || "anonymous"
2419
2511
  }), {
@@ -2432,11 +2524,11 @@ class ShokupanRouter {
2432
2524
  const ttl = config.middlewareTrackingTTL ?? 864e5;
2433
2525
  const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2434
2526
  const cutoff = Date.now() - ttl;
2435
- await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2436
- const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2527
+ await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2528
+ const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
2437
2529
  if (results?.[0]?.count > maxCapacity) {
2438
2530
  const toDelete = results[0].count - maxCapacity;
2439
- await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2531
+ await db.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2440
2532
  }
2441
2533
  } catch (datastoreError) {
2442
2534
  console.error("Failed to store middleware tracking:", datastoreError);
@@ -2466,7 +2558,8 @@ class ShokupanRouter {
2466
2558
  file,
2467
2559
  line
2468
2560
  },
2469
- controller
2561
+ controller,
2562
+ middleware: middleware || []
2470
2563
  });
2471
2564
  this.trie.insert(method, path2, bakedHandler);
2472
2565
  return this;
@@ -2595,7 +2688,8 @@ class ShokupanRouter {
2595
2688
  method,
2596
2689
  path: path2,
2597
2690
  spec,
2598
- handler: finalHandler
2691
+ handler: finalHandler,
2692
+ middleware: handlers.slice(0, handlers.length - 1)
2599
2693
  });
2600
2694
  }
2601
2695
  /**
@@ -2635,7 +2729,7 @@ class ShokupanRouter {
2635
2729
  }
2636
2730
  this.hooksInitialized = true;
2637
2731
  }
2638
- async runHooks(name, ...args) {
2732
+ runHooks(name, ...args) {
2639
2733
  if (!this.hooksInitialized) {
2640
2734
  this.ensureHooksInitialized();
2641
2735
  }
@@ -2644,7 +2738,7 @@ class ShokupanRouter {
2644
2738
  const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2645
2739
  const debug = ctx?.[$debug];
2646
2740
  if (debug) {
2647
- await Promise.all(fns.map(async (fn, index) => {
2741
+ return Promise.all(fns.map(async (fn, index) => {
2648
2742
  const hookId = `hook_${name}_${fn.name || index}`;
2649
2743
  const previousNode = debug.getCurrentNode();
2650
2744
  debug.trackEdge(previousNode, hookId);
@@ -2663,7 +2757,7 @@ class ShokupanRouter {
2663
2757
  }
2664
2758
  }));
2665
2759
  } else {
2666
- await Promise.all(fns.map((fn) => fn(...args)));
2760
+ return Promise.all(fns.map((fn) => fn(...args)));
2667
2761
  }
2668
2762
  }
2669
2763
  }
@@ -2710,6 +2804,100 @@ class SystemCpuMonitor {
2710
2804
  this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
2711
2805
  }
2712
2806
  }
2807
+ class SurrealDatastore {
2808
+ constructor(db) {
2809
+ this.db = db;
2810
+ process.on("exit", async () => {
2811
+ await this.disconnect();
2812
+ });
2813
+ }
2814
+ createSchema() {
2815
+ this.db.query(`
2816
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
2817
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
2818
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
2819
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
2820
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
2821
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
2822
+ DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
2823
+ `).collect();
2824
+ }
2825
+ /**
2826
+ * Select a record or contents of a table by its ID.
2827
+ */
2828
+ async select(id) {
2829
+ return this.db.select(id);
2830
+ }
2831
+ /**
2832
+ * Merge update data into a record by its ID.
2833
+ */
2834
+ async merge(id, data) {
2835
+ return this.db.update(id).merge(data).catch((err) => {
2836
+ if (err.message.includes("This transaction can be retried")) {
2837
+ return this.db.update(id).merge(data);
2838
+ }
2839
+ throw err;
2840
+ });
2841
+ }
2842
+ /**
2843
+ * Create a record by its ID.
2844
+ */
2845
+ async create(id, data) {
2846
+ return this.db.create(id).content(data).catch((err) => {
2847
+ if (err.message.includes("This transaction can be retried")) {
2848
+ return this.db.create(id).content(data);
2849
+ }
2850
+ throw err;
2851
+ });
2852
+ }
2853
+ /**
2854
+ * Upsert a record by its ID.
2855
+ */
2856
+ async upsert(id, data) {
2857
+ return this.db.upsert(id).content(data).catch((err) => {
2858
+ if (err.message.includes("This transaction can be retried")) {
2859
+ return this.db.upsert(id).content(data);
2860
+ }
2861
+ throw err;
2862
+ });
2863
+ }
2864
+ /**
2865
+ * Delete a record by its ID.
2866
+ */
2867
+ async delete(id) {
2868
+ return this.db.delete(id).catch((err) => {
2869
+ if (err.message.includes("This transaction can be retried")) {
2870
+ return this.db.delete(id);
2871
+ }
2872
+ throw err;
2873
+ });
2874
+ }
2875
+ /**
2876
+ * Run a SurrealDB query.
2877
+ */
2878
+ async query(query, vars) {
2879
+ return this.db.query(query, vars).collect().catch((err) => {
2880
+ if (err.message.includes("This transaction can be retried")) {
2881
+ return this.db.query(query, vars).collect();
2882
+ }
2883
+ throw err;
2884
+ });
2885
+ }
2886
+ /**
2887
+ * Create a relationship between two records.
2888
+ */
2889
+ async relate(fromId, edgeId, toId, data) {
2890
+ return this.db.relate(fromId, edgeId, toId, data).catch((err) => {
2891
+ if (err.message.includes("This transaction can be retried")) {
2892
+ return this.db.relate(fromId, edgeId, toId, data);
2893
+ }
2894
+ throw err;
2895
+ });
2896
+ }
2897
+ disconnect() {
2898
+ return this.db.close();
2899
+ }
2900
+ }
2713
2901
  const defaults = {
2714
2902
  port: 3e3,
2715
2903
  hostname: "localhost",
@@ -2722,9 +2910,15 @@ api.trace.getTracer("shokupan.application");
2722
2910
  class Shokupan extends ShokupanRouter {
2723
2911
  applicationConfig = {};
2724
2912
  openApiSpec;
2913
+ asyncApiSpec;
2725
2914
  composedMiddleware;
2726
2915
  cpuMonitor;
2727
2916
  server;
2917
+ datastore;
2918
+ dbPromise;
2919
+ get db() {
2920
+ return this.datastore;
2921
+ }
2728
2922
  get logger() {
2729
2923
  return this.applicationConfig.logger;
2730
2924
  }
@@ -2741,6 +2935,19 @@ class Shokupan extends ShokupanRouter {
2741
2935
  line,
2742
2936
  name: "ShokupanApplication"
2743
2937
  };
2938
+ this.dbPromise = this.initDatastore();
2939
+ }
2940
+ async initDatastore() {
2941
+ const db = new surrealdb.Surreal({ engines: this.applicationConfig.surreal?.engines ?? (await import("@surrealdb/node")).createNodeEngines() });
2942
+ this.datastore = new SurrealDatastore(db);
2943
+ await db.connect(
2944
+ this.applicationConfig.surreal?.url ?? (process.env.NODE_ENV === "test" ? "mem://" : "surrealkv://database"),
2945
+ this.applicationConfig.surreal?.connectOptions
2946
+ );
2947
+ await db.use({
2948
+ namespace: this.applicationConfig.surreal?.namespace ?? "vendor",
2949
+ database: this.applicationConfig.surreal?.database ?? "shokupan"
2950
+ });
2744
2951
  }
2745
2952
  /**
2746
2953
  * Adds middleware to the application.
@@ -2820,14 +3027,70 @@ class Shokupan extends ShokupanRouter {
2820
3027
  */
2821
3028
  async listen(port) {
2822
3029
  const finalPort = port ?? this.applicationConfig.port ?? 3e3;
2823
- if (finalPort < 0 || finalPort > 65535) {
3030
+ if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
2824
3031
  throw new Error("Invalid port number");
2825
3032
  }
2826
3033
  await Promise.all(this.startupHooks.map((hook) => hook()));
2827
3034
  if (this.applicationConfig.enableOpenApiGen) {
2828
3035
  this.openApiSpec = await generateOpenApi(this);
3036
+ this.get("/.well-known/openapi.yaml", (ctx) => {
3037
+ try {
3038
+ const yaml = jsYaml.dump(this.openApiSpec);
3039
+ return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
3040
+ } catch (e) {
3041
+ this.logger.error("Failed to generate OpenAPI YAML", { error: e });
3042
+ return ctx.text("Internal Server Error", 500);
3043
+ }
3044
+ });
3045
+ if (this.applicationConfig.aiPlugin?.enabled !== false) {
3046
+ this.get("/.well-known/ai-plugin.json", async (ctx) => {
3047
+ const config = this.applicationConfig.aiPlugin || {};
3048
+ let pkg = {};
3049
+ try {
3050
+ pkg = await Bun.file("package.json").json();
3051
+ } catch (e) {
3052
+ }
3053
+ const manifest = {
3054
+ schema_version: "v1",
3055
+ name_for_human: config.name_for_human || this.openApiSpec.info.title || pkg.name || "Shokupan App",
3056
+ name_for_model: config.name_for_model || this.openApiSpec.info.title || pkg.name || "Shokupan App",
3057
+ description_for_human: config.description_for_human || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
3058
+ description_for_model: config.description_for_model || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
3059
+ auth: config.auth || { type: "none" },
3060
+ api: config.api || {
3061
+ type: "openapi",
3062
+ url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`,
3063
+ is_user_authenticated: false
3064
+ },
3065
+ logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/logo.png`,
3066
+ // Placeholder default
3067
+ contact_email: config.contact_email || pkg.author?.email || "support@example.com",
3068
+ legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/legal`
3069
+ };
3070
+ return ctx.json(manifest);
3071
+ });
3072
+ }
3073
+ if (this.applicationConfig.apiCatalog?.enabled !== false) {
3074
+ this.get("/.well-known/api-catalog", (ctx) => {
3075
+ const config = this.applicationConfig.apiCatalog || {};
3076
+ const catalog = {
3077
+ versions: config.versions || [
3078
+ {
3079
+ name: this.openApiSpec.info.version || "v1",
3080
+ url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/`,
3081
+ spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`
3082
+ }
3083
+ ]
3084
+ };
3085
+ return ctx.json(catalog);
3086
+ });
3087
+ }
2829
3088
  await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
2830
3089
  }
3090
+ if (this.applicationConfig.enableAsyncApiGen) {
3091
+ const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
3092
+ this.asyncApiSpec = await generateAsyncApi2(this);
3093
+ }
2831
3094
  if (port === 0 && process.platform === "linux") ;
2832
3095
  if (this.applicationConfig.autoBackpressureFeedback === true) {
2833
3096
  this.cpuMonitor = new SystemCpuMonitor();
@@ -2887,6 +3150,8 @@ class Shokupan extends ShokupanRouter {
2887
3150
  });
2888
3151
  const ctx = new ShokupanContext(req, self.server);
2889
3152
  ctx[$ws] = ws;
3153
+ ws.data ??= {};
3154
+ ws.data["ctx"] = ctx;
2890
3155
  try {
2891
3156
  await handler(ctx);
2892
3157
  } catch (err) {
@@ -2906,6 +3171,15 @@ class Shokupan extends ShokupanRouter {
2906
3171
  },
2907
3172
  close(ws, code, reason) {
2908
3173
  ws.data?.handler?.close?.(ws, code, reason);
3174
+ const ctx = ws.data?.["ctx"];
3175
+ if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
3176
+ const callbacks = ctx.getDisconnectCallbacks();
3177
+ if (Array.isArray(callbacks) && callbacks.length > 0) {
3178
+ Promise.all(callbacks.map((cb) => cb())).catch((err) => {
3179
+ console.error("Error executing socket disconnect hook:", err);
3180
+ });
3181
+ }
3182
+ }
2909
3183
  }
2910
3184
  }
2911
3185
  };
@@ -2915,7 +3189,6 @@ class Shokupan extends ShokupanRouter {
2915
3189
  factory = createHttpServer();
2916
3190
  }
2917
3191
  this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2918
- console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
2919
3192
  return this.server;
2920
3193
  }
2921
3194
  /**
@@ -3047,9 +3320,6 @@ class Shokupan extends ShokupanRouter {
3047
3320
  await bodyParsing;
3048
3321
  return match.handler(ctx);
3049
3322
  }
3050
- if (ctx.upgrade()) {
3051
- return void 0;
3052
- }
3053
3323
  return null;
3054
3324
  });
3055
3325
  let response;
@@ -3065,6 +3335,9 @@ class Shokupan extends ShokupanRouter {
3065
3335
  } else if (ctx[$routeMatched]) {
3066
3336
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3067
3337
  } else {
3338
+ if (ctx.upgrade()) {
3339
+ return void 0;
3340
+ }
3068
3341
  if (ctx.response.status !== HTTP_STATUS.OK) {
3069
3342
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3070
3343
  } else {
@@ -3077,6 +3350,9 @@ class Shokupan extends ShokupanRouter {
3077
3350
  response = ctx.text(String(result));
3078
3351
  }
3079
3352
  await this.runHooks("onRequestEnd", ctx);
3353
+ if (response instanceof Promise) {
3354
+ response = await response;
3355
+ }
3080
3356
  await this.runHooks("onResponseStart", ctx, response);
3081
3357
  return response;
3082
3358
  } catch (err) {
@@ -3186,8 +3462,7 @@ function RateLimitMiddleware(options = {}) {
3186
3462
  }
3187
3463
  }
3188
3464
  const msg = typeof message === "function" ? message(ctx, key) : message;
3189
- typeof msg === "object" ? JSON.stringify(msg) : String(msg);
3190
- const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
3465
+ const res = await (typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode));
3191
3466
  if (headers) {
3192
3467
  setHeaders(res);
3193
3468
  res.headers.set("Retry-After", String(retryAfter));
@@ -3262,8 +3537,12 @@ function createMethodDecorator(method) {
3262
3537
  }
3263
3538
  target[$routeMethods].set(propertyKey, {
3264
3539
  method,
3265
- path: path2
3540
+ path: path2,
3541
+ source: getCallerInfo(2)
3266
3542
  });
3543
+ if (path2.includes("/user")) {
3544
+ console.log(`[Decorator] Captured source for ${propertyKey}:`, getCallerInfo());
3545
+ }
3267
3546
  };
3268
3547
  };
3269
3548
  }
@@ -3286,15 +3565,572 @@ function Event(eventName) {
3286
3565
  function RateLimit(options) {
3287
3566
  return Use(RateLimitMiddleware(options));
3288
3567
  }
3568
+ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3569
+ return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
3570
+ /* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
3571
+ /* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
3572
+ /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3573
+ /* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan AsyncAPI" }),
3574
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
3575
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
3576
+ /* @__PURE__ */ jsxRuntime.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" }),
3577
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
3578
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
3579
+ /* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
3580
+ __html: `
3581
+ window.INITIAL_SPEC = ${JSON.stringify(spec)};
3582
+ window.INITIAL_SERVER_URL = "${serverUrl}";
3583
+ window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
3584
+ `
3585
+ } })
3586
+ ] }),
3587
+ /* @__PURE__ */ jsxRuntime.jsxs("body", { children: [
3588
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "app-container", children: [
3589
+ /* @__PURE__ */ jsxRuntime.jsx(Sidebar, { navTree, disableSourceView }),
3590
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "resizer", id: "resizer-left" }),
3591
+ /* @__PURE__ */ jsxRuntime.jsx(MainContent, {}),
3592
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "resizer", id: "resizer-right" }),
3593
+ /* @__PURE__ */ jsxRuntime.jsx(ConsolePanel, { serverUrl })
3594
+ ] }),
3595
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.socket.io/4.7.4/socket.io.min.js" }),
3596
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
3597
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/asyncapi-client.mjs`, type: "module" })
3598
+ ] })
3599
+ ] });
3600
+ }
3601
+ function Sidebar({ navTree, disableSourceView }) {
3602
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar scroller", id: "sidebar", children: [
3603
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
3604
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "AsyncAPI" }),
3605
+ /* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-collapse-nav", class: "btn-icon", title: "Collapse Sidebar", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "15 18 9 12 15 6" }) }) })
3606
+ ] }),
3607
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
3608
+ ] });
3609
+ }
3610
+ function NavNode({ node, level, disableSourceView }) {
3611
+ const sortedEntries = Object.entries(node.children || {}).sort((a, b) => {
3612
+ const [aKey, aItem] = a;
3613
+ const [bKey, bItem] = b;
3614
+ const isWarningA = aItem.data?.op?.["x-warning"];
3615
+ const isWarningB = bItem.data?.op?.["x-warning"];
3616
+ if (isWarningA && !isWarningB) return -1;
3617
+ if (!isWarningA && isWarningB) return 1;
3618
+ if (aKey === bKey) return 0;
3619
+ if (aKey === "Warning" || aKey === "Warnings") return -1;
3620
+ if (bKey === "Warning" || bKey === "Warnings") return 1;
3621
+ if (aKey === "Application") return -1;
3622
+ if (bKey === "Application") return 1;
3623
+ if (aKey[0] === "/") return 1;
3624
+ if (bKey[0] === "/") return -1;
3625
+ return aKey.localeCompare(bKey);
3626
+ });
3627
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: sortedEntries.map(([key, item]) => {
3628
+ const hasChildren = Object.keys(item.children || {}).length > 0;
3629
+ if (level === 0) {
3630
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3631
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "group-label", children: key }),
3632
+ hasChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "tree-node", style: "margin-left: 0", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
3633
+ ] }, key);
3634
+ }
3635
+ const isLeaf = item.isLeaf;
3636
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3637
+ isLeaf ? /* @__PURE__ */ jsxRuntime.jsx(LeafNode, { item, label: key, disableSourceView }) : /* @__PURE__ */ jsxRuntime.jsx("div", { class: "tree-item", style: "color: var(--text-muted)", children: /* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: key }) }),
3638
+ hasChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "tree-node", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
3639
+ ] }, key);
3640
+ }) });
3641
+ }
3642
+ function LeafNode({ item, label, disableSourceView }) {
3643
+ const isWarning = item.data?.op?.["x-warning"];
3644
+ const opId = item.data?.name;
3645
+ const sourceInfo = item.data?.op?.["x-source-info"];
3646
+ let content;
3647
+ if (isWarning) {
3648
+ content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3649
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: "margin-right: 6px;", children: "⚠️" }),
3650
+ /* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: label })
3651
+ ] });
3652
+ } else {
3653
+ const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
3654
+ content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3655
+ /* @__PURE__ */ jsxRuntime.jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
3656
+ /* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: label })
3657
+ ] });
3658
+ }
3659
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "tree-item", "data-event": opId, style: isWarning ? "color: #fbbf24" : "", children: [
3660
+ content,
3661
+ sourceInfo && !disableSourceView && /* @__PURE__ */ jsxRuntime.jsx(
3662
+ "a",
3663
+ {
3664
+ href: `vscode://file/${sourceInfo.file}:${sourceInfo.line}`,
3665
+ class: "source-link",
3666
+ onClick: (e) => {
3667
+ e.stopPropagation();
3668
+ },
3669
+ title: `${sourceInfo.file}:${sourceInfo.line}`,
3670
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", style: "display:block", children: [
3671
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "16 18 22 12 16 6" }),
3672
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "8 6 2 12 8 18" })
3673
+ ] })
3674
+ }
3675
+ )
3676
+ ] });
3677
+ }
3678
+ function MainContent() {
3679
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "main-wrapper", style: "flex: 1; min-width: 0; position: relative; overflow: hidden;", children: [
3680
+ /* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-expand-nav", class: "btn-icon floating-toggle left", title: "Expand Sidebar", style: "display:none;", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
3681
+ /* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-expand-console", class: "btn-icon floating-toggle right", title: "Expand Console", style: "display:none;", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "15 18 9 12 15 6" }) }) }),
3682
+ /* @__PURE__ */ jsxRuntime.jsx("main", { class: "main-content scroller", id: "doc-panel", style: "height: 100%;", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "empty-state", children: [
3683
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", children: [
3684
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" }),
3685
+ /* @__PURE__ */ jsxRuntime.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" })
3686
+ ] }),
3687
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Select an event to view details" })
3688
+ ] }) })
3689
+ ] });
3690
+ }
3691
+ function ConsolePanel({ serverUrl }) {
3692
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "console-panel", id: "console-panel", children: [
3693
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "console-header", children: [
3694
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;", children: [
3695
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: "margin:0; font-size:1rem;", children: "Console" }),
3696
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display:flex; gap: 4px;", children: [
3697
+ /* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-maximize-console", class: "btn-icon", title: "Maximize Console", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }) }) }),
3698
+ /* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-collapse-console", class: "btn-icon", title: "Collapse Console", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }) })
3699
+ ] })
3700
+ ] }),
3701
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "connection-bar", children: [
3702
+ /* @__PURE__ */ jsxRuntime.jsxs("select", { id: "protocol", children: [
3703
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "ws", children: "WS" }),
3704
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "wss", children: "WSS" }),
3705
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "socket.io", children: "Socket.IO" })
3706
+ ] }),
3707
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: "width: 1px; background: rgba(255,255,255,0.1); margin: 2px 0;" }),
3708
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", id: "url", value: serverUrl })
3709
+ ] }),
3710
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display: grid; grid-template-columns: 1fr auto; gap: 8px;", children: [
3711
+ /* @__PURE__ */ jsxRuntime.jsx("button", { id: "connect-btn", class: "btn", children: "Connect" }),
3712
+ /* @__PURE__ */ jsxRuntime.jsx("button", { id: "clear-logs-btn", class: "btn secondary", title: "Clear Logs", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.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" }) }) })
3713
+ ] }),
3714
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "status-indicator", children: [
3715
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "status-dot", class: "dot" }),
3716
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: "connection-status", children: "Disconnected" })
3717
+ ] })
3718
+ ] }),
3719
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "logs-container scroller", id: "logs", children: /* @__PURE__ */ jsxRuntime.jsx("div", { class: "log-shim", id: "log-shim" }) }),
3720
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "compose-area", children: [
3721
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "compose-header", children: [
3722
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Payload" }),
3723
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: "target-event", style: "color: var(--primary);", children: "--" })
3724
+ ] }),
3725
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "editor-container" }),
3726
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "send-bar", children: /* @__PURE__ */ jsxRuntime.jsx("button", { id: "send-btn", class: "btn", children: "Send Message" }) })
3727
+ ] })
3728
+ ] });
3729
+ }
3730
+ function buildNavTree(spec) {
3731
+ if (!spec || !spec.channels) return { children: {} };
3732
+ const root = { children: {} };
3733
+ Object.keys(spec.channels).forEach((name) => {
3734
+ const ch = spec.channels[name];
3735
+ const op = ch.publish || ch.subscribe;
3736
+ const type = ch.publish ? "publish" : "subscribe";
3737
+ const tag = op.tags && op.tags.length > 0 ? op.tags[0].name : "General";
3738
+ if (!root.children[tag]) root.children[tag] = { children: {} };
3739
+ const parts = name.split(/[\.\/]/);
3740
+ let current = root.children[tag];
3741
+ parts.forEach((part, i) => {
3742
+ if (!current.children[part]) current.children[part] = { children: {} };
3743
+ current = current.children[part];
3744
+ if (i === parts.length - 1) {
3745
+ current.isLeaf = true;
3746
+ current.data = { name, op, type };
3747
+ }
3748
+ });
3749
+ });
3750
+ return root;
3751
+ }
3752
+ async function getAstRoutes(applications) {
3753
+ const astRoutes = [];
3754
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
3755
+ if (seen.has(app.name)) return [];
3756
+ const newSeen = new Set(seen);
3757
+ newSeen.add(app.name);
3758
+ const expanded = [];
3759
+ for (const route of app.routes) {
3760
+ expanded.push({
3761
+ ...route,
3762
+ // For events, path is the event name
3763
+ path: route.path.startsWith("/") ? route.path.slice(1) : route.path
3764
+ });
3765
+ }
3766
+ if (app.mounted) {
3767
+ for (const mount of app.mounted) {
3768
+ const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
3769
+ if (targetApp) {
3770
+ expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
3771
+ }
3772
+ }
3773
+ }
3774
+ return expanded;
3775
+ };
3776
+ applications.forEach((app) => {
3777
+ astRoutes.push(...getExpandedRoutes(app));
3778
+ });
3779
+ return astRoutes;
3780
+ }
3781
+ async function generateAsyncApi(rootRouter, options = {}) {
3782
+ const channels = {};
3783
+ let astRoutes = [];
3784
+ try {
3785
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-CKLGLFtx.cjs"));
3786
+ const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
3787
+ const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
3788
+ const { applications } = await analyzer2.analyze();
3789
+ astRoutes = await getAstRoutes(applications);
3790
+ } catch (e) {
3791
+ }
3792
+ const matchedAstRoutes = /* @__PURE__ */ new Set();
3793
+ const collect = async (router, prefix = "") => {
3794
+ const eventHandlers = router.getEventHandlers();
3795
+ let routerTag = "Other";
3796
+ if (router[$isApplication]) {
3797
+ routerTag = "Application";
3798
+ } else if (router.constructor.name && router.constructor.name !== "ShokupanRouter") {
3799
+ routerTag = router.constructor.name;
3800
+ } else {
3801
+ routerTag = router[$mountPath] || "Router";
3802
+ }
3803
+ if (eventHandlers) {
3804
+ for (const [eventName, handlers] of eventHandlers.entries()) {
3805
+ for (const handler of handlers) {
3806
+ const userSpec = handler.spec;
3807
+ let tags = userSpec?.tags;
3808
+ if (!tags && routerTag) {
3809
+ tags = [{ name: routerTag }];
3810
+ }
3811
+ let astMatch = astRoutes.find(
3812
+ (r) => (r.method === "EVENT" || r.method === "ON") && r.path === eventName
3813
+ );
3814
+ if (!astMatch) {
3815
+ const runtimeSource = (handler.originalHandler || handler).toString();
3816
+ const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
3817
+ const normalize = (s) => stripComments(s).replace(/\s+/g, "");
3818
+ const runtimeHandlerSrc = normalize(runtimeSource);
3819
+ const eventRoutes = astRoutes.filter((r) => r.method === "EVENT" || r.method === "ON");
3820
+ astMatch = eventRoutes.find((r) => {
3821
+ const astHandlerSrc = normalize(r.handlerSource || r.handlerName || "");
3822
+ if (!astHandlerSrc || astHandlerSrc.length < 5) return false;
3823
+ return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(normalize(r.handlerSource).substring(0, 50));
3824
+ });
3825
+ }
3826
+ if (astMatch) matchedAstRoutes.add(astMatch);
3827
+ const sourceInfo = handler.source || astMatch?.sourceContext ? {
3828
+ file: handler.source?.file || astMatch?.sourceContext?.file,
3829
+ line: handler.source?.line || astMatch?.sourceContext?.startLine,
3830
+ startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
3831
+ endLine: astMatch?.sourceContext?.endLine,
3832
+ highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
3833
+ } : void 0;
3834
+ if (!channels[eventName]) {
3835
+ channels[eventName] = {
3836
+ publish: {
3837
+ operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
3838
+ tags,
3839
+ message: {
3840
+ payload: { type: "object" },
3841
+ ...userSpec?.message ? userSpec.message : {}
3842
+ },
3843
+ ...userSpec?.type === "publish" ? userSpec : {},
3844
+ "x-source-info": sourceInfo ? [sourceInfo] : [],
3845
+ "x-shokupan-source": sourceInfo
3846
+ // Simplified
3847
+ }
3848
+ };
3849
+ if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
3850
+ if (userSpec?.description) channels[eventName].publish.description = userSpec.description;
3851
+ } else {
3852
+ if (sourceInfo) {
3853
+ if (!channels[eventName].publish["x-source-info"]) {
3854
+ channels[eventName].publish["x-source-info"] = [];
3855
+ }
3856
+ const exists = channels[eventName].publish["x-source-info"].some(
3857
+ (s) => s.file === sourceInfo.file && s.line === sourceInfo.line
3858
+ );
3859
+ if (!exists) {
3860
+ channels[eventName].publish["x-source-info"].push(sourceInfo);
3861
+ }
3862
+ }
3863
+ }
3864
+ let emits = astMatch?.emits || [];
3865
+ for (const emit of emits) {
3866
+ if (emit.event === "__DYNAMIC_EMIT__") {
3867
+ const warningKey = `${eventName}/Dynamic Emit`;
3868
+ channels[warningKey] = {
3869
+ subscribe: {
3870
+ operationId: `dynamicEmitWarning${eventName}`,
3871
+ summary: "Dynamic Emit Detected",
3872
+ description: "This handler emits an event with a dynamic name that could not be determined statically.",
3873
+ tags,
3874
+ "x-warning": true,
3875
+ "x-source-info": {
3876
+ file: astMatch?.sourceContext?.file,
3877
+ line: emit.location?.startLine,
3878
+ startLine: emit.location?.startLine,
3879
+ endLine: emit.location?.endLine,
3880
+ highlightLines: emit.location ? [emit.location.startLine, emit.location.endLine] : void 0
3881
+ },
3882
+ "x-shokupan-source": {
3883
+ file: astMatch?.sourceContext?.file,
3884
+ line: emit.location?.startLine
3885
+ },
3886
+ message: { payload: { type: "object" } }
3887
+ }
3888
+ };
3889
+ continue;
3890
+ }
3891
+ const emitStart = emit.location?.startLine;
3892
+ const emitEnd = emit.location?.endLine;
3893
+ const newSourceInfo = sourceInfo && emitStart ? {
3894
+ file: sourceInfo.file,
3895
+ line: emitStart,
3896
+ startLine: emitStart,
3897
+ endLine: emitEnd,
3898
+ highlightLines: sourceInfo.highlightLines,
3899
+ emitHighlightLines: [emitStart, emitEnd]
3900
+ } : void 0;
3901
+ if (!channels[emit.event]) {
3902
+ channels[emit.event] = {
3903
+ subscribe: {
3904
+ operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
3905
+ tags,
3906
+ message: {
3907
+ payload: emit.payload || { type: "object" }
3908
+ },
3909
+ "x-source-info": newSourceInfo ? [newSourceInfo] : [],
3910
+ "x-shokupan-source": sourceInfo && emitStart ? {
3911
+ file: sourceInfo.file,
3912
+ line: emitStart
3913
+ } : void 0
3914
+ }
3915
+ };
3916
+ } else {
3917
+ if (newSourceInfo) {
3918
+ if (!channels[emit.event].subscribe["x-source-info"]) {
3919
+ channels[emit.event].subscribe["x-source-info"] = [];
3920
+ }
3921
+ const existing = channels[emit.event].subscribe["x-source-info"];
3922
+ const exists = existing.some(
3923
+ (s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
3924
+ );
3925
+ if (!exists) {
3926
+ existing.push(newSourceInfo);
3927
+ }
3928
+ }
3929
+ }
3930
+ }
3931
+ }
3932
+ }
3933
+ }
3934
+ const httpRoutes = router[$routes];
3935
+ if (httpRoutes) {
3936
+ for (const route of httpRoutes) {
3937
+ const handler = route.handler;
3938
+ let tags = route.handlerSpec?.tags;
3939
+ if (!tags && routerTag) {
3940
+ tags = [{ name: routerTag }];
3941
+ }
3942
+ const methodUpper = route.method.toUpperCase();
3943
+ let astMatch = astRoutes.find(
3944
+ (r) => r.method === methodUpper && (r.path === route.path || r.path === "/" + route.path)
3945
+ );
3946
+ if (!astMatch) {
3947
+ const runtimeSource = (handler.originalHandler || handler).toString();
3948
+ const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
3949
+ const sameMethodRoutes = astRoutes.filter((r) => r.method === methodUpper);
3950
+ astMatch = sameMethodRoutes.find((r) => {
3951
+ const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
3952
+ if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
3953
+ return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
3954
+ });
3955
+ }
3956
+ const sourceInfo = handler.source || astMatch?.sourceContext ? {
3957
+ file: handler.source?.file || astMatch?.sourceContext?.file,
3958
+ line: handler.source?.line || astMatch?.sourceContext?.startLine,
3959
+ startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
3960
+ endLine: astMatch?.sourceContext?.endLine,
3961
+ highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
3962
+ } : void 0;
3963
+ let emits = astMatch?.emits || [];
3964
+ for (const emit of emits) {
3965
+ const emitStart = emit.location?.startLine;
3966
+ const emitEnd = emit.location?.endLine;
3967
+ const newSourceInfo = sourceInfo && emitStart ? {
3968
+ file: sourceInfo.file,
3969
+ line: emitStart,
3970
+ startLine: emitStart,
3971
+ endLine: emitEnd,
3972
+ highlightLines: sourceInfo.highlightLines,
3973
+ emitHighlightLines: [emitStart, emitEnd]
3974
+ } : void 0;
3975
+ if (!channels[emit.event]) {
3976
+ channels[emit.event] = {
3977
+ subscribe: {
3978
+ operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
3979
+ tags,
3980
+ message: {
3981
+ payload: emit.payload || { type: "object" }
3982
+ },
3983
+ "x-source-info": newSourceInfo ? [newSourceInfo] : [],
3984
+ "x-shokupan-source": sourceInfo && emitStart ? {
3985
+ file: sourceInfo.file,
3986
+ line: emitStart
3987
+ } : void 0
3988
+ }
3989
+ };
3990
+ } else {
3991
+ if (newSourceInfo) {
3992
+ if (!channels[emit.event].subscribe["x-source-info"]) {
3993
+ channels[emit.event].subscribe["x-source-info"] = [];
3994
+ }
3995
+ const existing = channels[emit.event].subscribe["x-source-info"];
3996
+ const exists = existing.some(
3997
+ (s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
3998
+ );
3999
+ if (!exists) {
4000
+ existing.push(newSourceInfo);
4001
+ }
4002
+ }
4003
+ }
4004
+ }
4005
+ }
4006
+ }
4007
+ const childRouters = router[$childRouters];
4008
+ for (const child of childRouters) {
4009
+ await collect(child);
4010
+ }
4011
+ };
4012
+ await collect(rootRouter);
4013
+ const dynamicEvents = astRoutes.filter((r) => r.path === "__DYNAMIC_EVENT__" && !matchedAstRoutes.has(r));
4014
+ dynamicEvents.forEach((r, i) => {
4015
+ let prefix = "Anonymous";
4016
+ if (r.handlerName && !r.handlerName.includes("=>") && !r.handlerName.includes("{")) {
4017
+ const parts = r.handlerName.split(".");
4018
+ if (parts.length > 0) prefix = parts[0];
4019
+ }
4020
+ const key = `${prefix}.Dynamic Event ${i + 1}`;
4021
+ channels[key] = {
4022
+ publish: {
4023
+ operationId: `dynamicEventWarning${i}`,
4024
+ summary: "Dynamic Event Detected",
4025
+ description: `A dynamic event listener was detected in your source code but the event name could not be determined statically.`,
4026
+ tags: [{ name: "Warnings" }],
4027
+ "x-warning": true,
4028
+ "x-source-info": {
4029
+ file: r.sourceContext?.file,
4030
+ line: r.sourceContext?.startLine,
4031
+ startLine: r.sourceContext?.startLine,
4032
+ endLine: r.sourceContext?.endLine,
4033
+ highlightLines: r.sourceContext ? [r.sourceContext.startLine, r.sourceContext.endLine] : void 0
4034
+ },
4035
+ "x-shokupan-source": {
4036
+ file: r.sourceContext?.file,
4037
+ line: r.sourceContext?.startLine
4038
+ },
4039
+ message: { payload: { type: "object" } }
4040
+ }
4041
+ };
4042
+ });
4043
+ return {
4044
+ asyncapi: "3.0.0",
4045
+ info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
4046
+ channels
4047
+ };
4048
+ }
4049
+ const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
4050
+ __proto__: null,
4051
+ generateAsyncApi
4052
+ }, Symbol.toStringTag, { value: "Module" }));
4053
+ class AsyncApiPlugin extends ShokupanRouter {
4054
+ constructor(pluginOptions = {}) {
4055
+ super({ renderer: renderToString });
4056
+ this.pluginOptions = pluginOptions;
4057
+ this.pluginOptions.path ??= "/asyncapi";
4058
+ this.init();
4059
+ }
4060
+ static getBasePath() {
4061
+ const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
4062
+ if (dir.endsWith("dist")) {
4063
+ return dir + "/plugins/application/asyncapi";
4064
+ }
4065
+ return dir;
4066
+ }
4067
+ onInit(app, options) {
4068
+ const path2 = this.pluginOptions.path || options?.path || "/asyncapi";
4069
+ app.mount(path2, this);
4070
+ if (app.applicationConfig.enableAsyncApiGen !== true) {
4071
+ console.warn("AsyncApiPlugin: enableAsyncApiGen is disabled. AsyncApiPlugin will not generate spec.");
4072
+ }
4073
+ }
4074
+ init() {
4075
+ const serveFile = async (ctx, file, type) => {
4076
+ const content = await promises.readFile(path$1.join(AsyncApiPlugin.getBasePath(), "static", file), "utf-8");
4077
+ ctx.set("Content-Type", type);
4078
+ return ctx.send(content);
4079
+ };
4080
+ this.get("/style.css", (ctx) => serveFile(ctx, "style.css", "text/css"));
4081
+ this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
4082
+ this.get("/asyncapi-client.mjs", (ctx) => serveFile(ctx, "asyncapi-client.mjs", "application/javascript"));
4083
+ this.get("/", async (ctx) => {
4084
+ let spec = ctx.app?.asyncApiSpec;
4085
+ if (!spec) {
4086
+ spec = await generateAsyncApi(ctx.app);
4087
+ }
4088
+ if (this.pluginOptions.spec) {
4089
+ deepMerge(spec, this.pluginOptions.spec);
4090
+ }
4091
+ const serverUrl = `${ctx.hostname}:${ctx.app?.applicationConfig.port}`;
4092
+ const base = this.pluginOptions.path;
4093
+ const disableSourceView = this.pluginOptions.disableSourceView;
4094
+ const navTree = buildNavTree(spec);
4095
+ return ctx.jsx(AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }));
4096
+ });
4097
+ this.get("/json", async (ctx) => {
4098
+ let spec = ctx.app?.asyncApiSpec;
4099
+ if (!spec) {
4100
+ spec = await generateAsyncApi(ctx.app);
4101
+ }
4102
+ if (this.pluginOptions.spec) {
4103
+ deepMerge(spec, this.pluginOptions.spec);
4104
+ }
4105
+ return ctx.json(spec);
4106
+ });
4107
+ this.get("/_code", async (ctx) => {
4108
+ const file = ctx.query["file"];
4109
+ if (!file || typeof file !== "string") {
4110
+ return ctx.text("Missing file parameter", 400);
4111
+ }
4112
+ try {
4113
+ const content = await promises.readFile(file, "utf8");
4114
+ return ctx.text(content);
4115
+ } catch (e) {
4116
+ return ctx.text("File not found: " + e.message, 404);
4117
+ }
4118
+ });
4119
+ }
4120
+ }
3289
4121
  class AuthPlugin extends ShokupanRouter {
3290
4122
  constructor(authConfig) {
3291
4123
  super();
3292
4124
  this.authConfig = authConfig;
3293
4125
  this.secret = typeof authConfig.jwtSecret === "string" ? new TextEncoder().encode(authConfig.jwtSecret) : authConfig.jwtSecret;
3294
- this.init();
3295
4126
  }
3296
4127
  secret;
3297
- onInit(app, options) {
4128
+ arctic;
4129
+ jose;
4130
+ async onInit(app, options) {
4131
+ this.arctic = await import("arctic");
4132
+ this.jose = await import("jose");
4133
+ this.init();
3298
4134
  if (options?.path) {
3299
4135
  app.mount(options.path, this);
3300
4136
  } else {
@@ -3302,15 +4138,16 @@ class AuthPlugin extends ShokupanRouter {
3302
4138
  }
3303
4139
  }
3304
4140
  getProviderInstance(name, p) {
4141
+ const { GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
3305
4142
  switch (name) {
3306
4143
  case "github":
3307
- return new arctic.GitHub(p.clientId, p.clientSecret, p.redirectUri);
4144
+ return new GitHub(p.clientId, p.clientSecret, p.redirectUri);
3308
4145
  case "google":
3309
- return new arctic.Google(p.clientId, p.clientSecret, p.redirectUri);
4146
+ return new Google(p.clientId, p.clientSecret, p.redirectUri);
3310
4147
  case "microsoft":
3311
- return new arctic.MicrosoftEntraId(p.tenantId, p.clientId, p.clientSecret, p.redirectUri);
4148
+ return new MicrosoftEntraId(p.tenantId, p.clientId, p.clientSecret, p.redirectUri);
3312
4149
  case "apple":
3313
- return new arctic.Apple(
4150
+ return new Apple(
3314
4151
  p.clientId,
3315
4152
  p.teamId,
3316
4153
  p.keyId,
@@ -3318,18 +4155,18 @@ class AuthPlugin extends ShokupanRouter {
3318
4155
  p.redirectUri
3319
4156
  );
3320
4157
  case "auth0":
3321
- return new arctic.Auth0(p.domain, p.clientId, p.clientSecret, p.redirectUri);
4158
+ return new Auth0(p.domain, p.clientId, p.clientSecret, p.redirectUri);
3322
4159
  case "okta":
3323
- return new arctic.Okta(p.domain, p.authUrl, p.clientId, p.clientSecret, p.redirectUri);
4160
+ return new Okta(p.domain, p.authUrl, p.clientId, p.clientSecret, p.redirectUri);
3324
4161
  case "oauth2":
3325
- return new arctic.OAuth2Client(p.clientId, p.clientSecret, p.redirectUri);
4162
+ return new OAuth2Client(p.clientId, p.clientSecret, p.redirectUri);
3326
4163
  default:
3327
4164
  return null;
3328
4165
  }
3329
4166
  }
3330
4167
  async createSession(user, ctx) {
3331
4168
  const alg = "HS256";
3332
- const jwt = await new jose__namespace.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
4169
+ const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
3333
4170
  const opts = this.authConfig.cookieOptions || {};
3334
4171
  let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
3335
4172
  if (opts.secure) cookie += "; Secure";
@@ -3339,6 +4176,7 @@ class AuthPlugin extends ShokupanRouter {
3339
4176
  return jwt;
3340
4177
  }
3341
4178
  init() {
4179
+ const { generateState, generateCodeVerifier, GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
3342
4180
  const providerEntries = Object.entries(this.authConfig.providers);
3343
4181
  for (let i = 0; i < providerEntries.length; i++) {
3344
4182
  const [providerName, providerConfig] = providerEntries[i];
@@ -3348,17 +4186,17 @@ class AuthPlugin extends ShokupanRouter {
3348
4186
  continue;
3349
4187
  }
3350
4188
  this.get(`/auth/${providerName}/login`, async (ctx) => {
3351
- const state = arctic.generateState();
3352
- const codeVerifier = providerName === "google" || providerName === "microsoft" || providerName === "auth0" || providerName === "okta" ? arctic.generateCodeVerifier() : void 0;
4189
+ const state = generateState();
4190
+ const codeVerifier = providerName === "google" || providerName === "microsoft" || providerName === "auth0" || providerName === "okta" ? generateCodeVerifier() : void 0;
3353
4191
  const scopes = providerConfig.scopes || [];
3354
4192
  let url;
3355
- if (provider instanceof arctic.GitHub) {
4193
+ if (provider instanceof GitHub) {
3356
4194
  url = await provider.createAuthorizationURL(state, scopes);
3357
- } else if (provider instanceof arctic.Google || provider instanceof arctic.MicrosoftEntraId || provider instanceof arctic.Auth0 || provider instanceof arctic.Okta) {
4195
+ } else if (provider instanceof Google || provider instanceof MicrosoftEntraId || provider instanceof Auth0 || provider instanceof Okta) {
3358
4196
  url = await provider.createAuthorizationURL(state, codeVerifier, scopes);
3359
- } else if (provider instanceof arctic.Apple) {
4197
+ } else if (provider instanceof Apple) {
3360
4198
  url = await provider.createAuthorizationURL(state, scopes);
3361
- } else if (provider instanceof arctic.OAuth2Client) {
4199
+ } else if (provider instanceof OAuth2Client) {
3362
4200
  if (!providerConfig.authUrl) return ctx.text("Config error: authUrl required for oauth2", 500);
3363
4201
  url = await provider.createAuthorizationURL(providerConfig.authUrl, state, scopes);
3364
4202
  } else {
@@ -3384,17 +4222,17 @@ class AuthPlugin extends ShokupanRouter {
3384
4222
  try {
3385
4223
  let tokens;
3386
4224
  let idToken;
3387
- if (provider instanceof arctic.GitHub) {
4225
+ if (provider instanceof GitHub) {
3388
4226
  tokens = await provider.validateAuthorizationCode(code);
3389
- } else if (provider instanceof arctic.Google || provider instanceof arctic.MicrosoftEntraId) {
4227
+ } else if (provider instanceof Google || provider instanceof MicrosoftEntraId) {
3390
4228
  if (!storedVerifier) return ctx.text("Missing verifier", 400);
3391
4229
  tokens = await provider.validateAuthorizationCode(code, storedVerifier);
3392
- } else if (provider instanceof arctic.Auth0 || provider instanceof arctic.Okta) {
4230
+ } else if (provider instanceof Auth0 || provider instanceof Okta) {
3393
4231
  tokens = await provider.validateAuthorizationCode(code, storedVerifier || "");
3394
- } else if (provider instanceof arctic.Apple) {
4232
+ } else if (provider instanceof Apple) {
3395
4233
  tokens = await provider.validateAuthorizationCode(code);
3396
4234
  idToken = tokens.idToken;
3397
- } else if (provider instanceof arctic.OAuth2Client) {
4235
+ } else if (provider instanceof OAuth2Client) {
3398
4236
  if (!providerConfig.tokenUrl) return ctx.text("Config error: tokenUrl required for oauth2", 500);
3399
4237
  tokens = await provider.validateAuthorizationCode(providerConfig.tokenUrl, code, null);
3400
4238
  }
@@ -3470,7 +4308,7 @@ class AuthPlugin extends ShokupanRouter {
3470
4308
  };
3471
4309
  } else if (provider === "apple") {
3472
4310
  if (idToken) {
3473
- const payload = jose__namespace.decodeJwt(idToken);
4311
+ const payload = this.jose.decodeJwt(idToken);
3474
4312
  user = {
3475
4313
  id: payload.sub,
3476
4314
  email: payload["email"],
@@ -3501,6 +4339,9 @@ class AuthPlugin extends ShokupanRouter {
3501
4339
  */
3502
4340
  getMiddleware() {
3503
4341
  return async (ctx, next) => {
4342
+ if (!this.jose) {
4343
+ this.jose = await import("jose");
4344
+ }
3504
4345
  const authHeader = ctx.req.headers.get("Authorization");
3505
4346
  let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
3506
4347
  if (!token) {
@@ -3509,7 +4350,7 @@ class AuthPlugin extends ShokupanRouter {
3509
4350
  }
3510
4351
  if (token) {
3511
4352
  try {
3512
- const { payload } = await jose__namespace.jwtVerify(token, this.secret);
4353
+ const { payload } = await this.jose.jwtVerify(token, this.secret);
3513
4354
  ctx.user = payload;
3514
4355
  } catch {
3515
4356
  }
@@ -3626,6 +4467,187 @@ class ClusterPlugin {
3626
4467
  }
3627
4468
  }
3628
4469
  }
4470
+ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
4471
+ return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
4472
+ /* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
4473
+ /* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
4474
+ /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
4475
+ /* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan Debug Dashboard" }),
4476
+ /* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
4477
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
4478
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
4479
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/styles.css` }),
4480
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/reactflow.css` }),
4481
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
4482
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
4483
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
4484
+ /* @__PURE__ */ jsxRuntime.jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
4485
+ ] }),
4486
+ /* @__PURE__ */ jsxRuntime.jsxs("body", { children: [
4487
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "container", children: [
4488
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
4489
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4490
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { children: "Dashboard" }),
4491
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary)", children: [
4492
+ "Uptime: ",
4493
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: "uptime", children: uptime })
4494
+ ] })
4495
+ ] }),
4496
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "tabs", children: [
4497
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
4498
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
4499
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
4500
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
4501
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
4502
+ integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
4503
+ integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
4504
+ ] })
4505
+ ] }),
4506
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
4507
+ /* @__PURE__ */ jsxRuntime.jsx(MetricsGrid, { metrics }),
4508
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
4509
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxRuntime.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: [
4510
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1m", children: "1 Minute" }),
4511
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "5m", children: "5 Minutes" }),
4512
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "30m", children: "30 Minutes" }),
4513
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1h", children: "1 Hour" }),
4514
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "2h", children: "2 Hours" }),
4515
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "6h", children: "6 Hours" }),
4516
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "12h", children: "12 Hours" }),
4517
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1d", children: "1 Day" }),
4518
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "3d", children: "3 Days" }),
4519
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "7d", children: "7 Days" }),
4520
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "30d", children: "30 Days" })
4521
+ ] }) }),
4522
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
4523
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
4524
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
4525
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
4526
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
4527
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
4528
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
4529
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
4530
+ ] }),
4531
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
4532
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
4533
+ /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
4534
+ /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
4535
+ /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
4536
+ /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
4537
+ ] }),
4538
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-table", class: "table-dark" }) })
4539
+ ] })
4540
+ ] }),
4541
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
4542
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Component Registry" }),
4543
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
4544
+ ] }) }),
4545
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-graph", class: "tab-content", children: [
4546
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsxRuntime.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);" }) }) }),
4547
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "cy" })
4548
+ ] }),
4549
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-requests", class: "tab-content", children: [
4550
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4551
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
4552
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.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" }) })
4553
+ ] }),
4554
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
4555
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
4556
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Request Details" }),
4557
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "request-details-content" }),
4558
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
4559
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "middleware-trace-container" })
4560
+ ] })
4561
+ ] }),
4562
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-failures", class: "tab-content", children: [
4563
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4564
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
4565
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4566
+ /* @__PURE__ */ jsxRuntime.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" }),
4567
+ /* @__PURE__ */ jsxRuntime.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" })
4568
+ ] })
4569
+ ] }),
4570
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "failures-table-container" })
4571
+ ] }),
4572
+ integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-scalar", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
4573
+ integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
4574
+ ] }),
4575
+ /* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
4576
+ __html: `
4577
+ // Injected function from server config
4578
+ const getRequestHeaders = ${getRequestHeadersSource};
4579
+ `
4580
+ } }),
4581
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/poll.js` }),
4582
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
4583
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/charts.js` }),
4584
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tables.js` }),
4585
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/registry.js` }),
4586
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/failures.js` }),
4587
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/requests.js` }),
4588
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tabs.js` })
4589
+ ] })
4590
+ ] });
4591
+ }
4592
+ function MetricsGrid({ metrics }) {
4593
+ const total = metrics.totalRequests;
4594
+ const active = metrics.activeRequests;
4595
+ const finished = total - active;
4596
+ const successRate = finished ? Math.round(metrics.successfulRequests / finished * 100) : 100;
4597
+ const failRate = finished ? Math.round(metrics.failedRequests / finished * 100) : 0;
4598
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "metrics-grid", children: [
4599
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
4600
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Total Requests" }),
4601
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value", id: "total-requests", children: metrics.totalRequests })
4602
+ ] }),
4603
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
4604
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Active Requests" }),
4605
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value", style: "color: var(--accent)", id: "active-requests", children: metrics.activeRequests })
4606
+ ] }),
4607
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
4608
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Success Rate" }),
4609
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value text-success", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { id: "success-rate", children: [
4610
+ successRate,
4611
+ "%"
4612
+ ] }) }),
4613
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
4614
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: "successful-requests", children: metrics.successfulRequests }),
4615
+ " successful"
4616
+ ] })
4617
+ ] }),
4618
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
4619
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Fail Rate" }),
4620
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value text-error", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { id: "fail-rate", children: [
4621
+ failRate,
4622
+ "%"
4623
+ ] }) }),
4624
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
4625
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: "failed-requests", children: metrics.failedRequests }),
4626
+ " failed"
4627
+ ] })
4628
+ ] }),
4629
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
4630
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Avg Latency" }),
4631
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-value", children: [
4632
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: "avg-latency", children: metrics.averageTotalTime_ms.toFixed(2) }),
4633
+ " ",
4634
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: "font-size: 1rem; color: var(--text-secondary)", children: "ms" })
4635
+ ] })
4636
+ ] })
4637
+ ] });
4638
+ }
4639
+ function ChartCard({ title, id }) {
4640
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "height: 300px;", children: [
4641
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: title }),
4642
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-chart", children: /* @__PURE__ */ jsxRuntime.jsx("canvas", { id }) })
4643
+ ] });
4644
+ }
4645
+ function Card({ title, contentId }) {
4646
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
4647
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: title }),
4648
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: contentId })
4649
+ ] });
4650
+ }
3629
4651
  const INTERVALS = [
3630
4652
  { label: "10s", ms: 10 * 1e3 },
3631
4653
  { label: "1m", ms: 60 * 1e3 },
@@ -3640,19 +4662,19 @@ const INTERVALS = [
3640
4662
  { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
3641
4663
  ];
3642
4664
  class MetricsCollector {
3643
- currentIntervalStart = {};
3644
- pendingDetails = {};
3645
- eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
3646
- timer = null;
3647
- constructor() {
4665
+ constructor(db) {
4666
+ this.db = db;
3648
4667
  this.eventLoopHistogram.enable();
3649
4668
  const now = Date.now();
3650
4669
  INTERVALS.forEach((int) => {
3651
4670
  this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3652
4671
  this.pendingDetails[int.label] = [];
3653
4672
  });
3654
- this.timer = setInterval(() => this.collect(), 1e4);
3655
4673
  }
4674
+ currentIntervalStart = {};
4675
+ pendingDetails = {};
4676
+ eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
4677
+ timer = null;
3656
4678
  recordRequest(duration, isError) {
3657
4679
  INTERVALS.forEach((int) => {
3658
4680
  this.pendingDetails[int.label].push({ duration, isError });
@@ -3664,11 +4686,9 @@ class MetricsCollector {
3664
4686
  async collect() {
3665
4687
  try {
3666
4688
  const now = Date.now();
3667
- console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
3668
4689
  for (const int of INTERVALS) {
3669
4690
  const start = this.currentIntervalStart[int.label];
3670
4691
  if (now >= start + int.ms) {
3671
- console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
3672
4692
  await this.flushInterval(int.label, start, int.ms);
3673
4693
  this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3674
4694
  }
@@ -3679,10 +4699,8 @@ class MetricsCollector {
3679
4699
  }
3680
4700
  async flushInterval(label, timestamp, durationMs) {
3681
4701
  const reqs = this.pendingDetails[label];
3682
- console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
3683
4702
  this.pendingDetails[label] = [];
3684
4703
  if (reqs.length === 0) {
3685
- console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
3686
4704
  return;
3687
4705
  }
3688
4706
  const totalReqs = reqs.length;
@@ -3732,15 +4750,11 @@ class MetricsCollector {
3732
4750
  p99: getP(0.99)
3733
4751
  }
3734
4752
  };
3735
- console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
3736
4753
  try {
3737
4754
  const recordId = new surrealdb.RecordId("metrics", timestamp);
3738
- await datastore.set(recordId, metric);
3739
- console.log(`[MetricsCollector] Successfully saved ${label} metric to datastore`);
3740
- const test = await datastore.get(recordId);
3741
- console.log(`[MetricsCollector] DEBUG: Immediate .get() returned:`, test ? "DATA" : "NULL");
3742
- const queryTest = await datastore.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
3743
- console.log(`[MetricsCollector] DEBUG: Query by id returned ${queryTest[0]?.length || 0} records`);
4755
+ await this.db.upsert(recordId, metric);
4756
+ const test = await this.db.select(recordId);
4757
+ const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
3744
4758
  } catch (e) {
3745
4759
  console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
3746
4760
  }
@@ -3775,16 +4789,8 @@ class Dashboard {
3775
4789
  constructor(dashboardConfig = {}) {
3776
4790
  this.dashboardConfig = dashboardConfig;
3777
4791
  }
3778
- static __dirname = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
3779
- // Get base path for dashboard files - works in both dev (src/) and production (dist/)
3780
- static getBasePath() {
3781
- const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
3782
- if (dir.endsWith("dist")) {
3783
- return dir + "/plugins/application/dashboard";
3784
- }
3785
- return dir;
3786
- }
3787
- router = new ShokupanRouter();
4792
+ [$appRoot];
4793
+ router = new ShokupanRouter({ renderer: renderToString });
3788
4794
  metrics = {
3789
4795
  totalRequests: 0,
3790
4796
  successfulRequests: 0,
@@ -3797,16 +4803,16 @@ class Dashboard {
3797
4803
  nodeMetrics: {},
3798
4804
  edgeMetrics: {}
3799
4805
  };
3800
- eta = new eta$2.Eta({
3801
- views: Dashboard.getBasePath() + "/static",
3802
- cache: false
3803
- });
3804
4806
  startTime = Date.now();
3805
4807
  instrumented = false;
3806
- metricsCollector = new MetricsCollector();
4808
+ metricsCollector;
4809
+ get db() {
4810
+ return this[$appRoot].db;
4811
+ }
3807
4812
  // ShokupanPlugin interface implementation
3808
4813
  onInit(app, options) {
3809
4814
  this[$appRoot] = app;
4815
+ this.metricsCollector = new MetricsCollector(this.db);
3810
4816
  const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
3811
4817
  const hooks = this.getHooks();
3812
4818
  if (!app.middleware) {
@@ -3826,6 +4832,47 @@ class Dashboard {
3826
4832
  app.mount(mountPath, this.router);
3827
4833
  this.setupRoutes();
3828
4834
  }
4835
+ detectIntegrations() {
4836
+ const integrations = {};
4837
+ const routers = this[$appRoot]?.[$childRouters] || [];
4838
+ const checkConfig = (key) => {
4839
+ const conf = this.dashboardConfig.integrations?.[key];
4840
+ if (conf === false) return { enabled: false };
4841
+ if (typeof conf === "object" && conf.path) return { enabled: true, path: conf.path };
4842
+ return { enabled: true };
4843
+ };
4844
+ const scalarConf = checkConfig("scalar");
4845
+ if (scalarConf.enabled) {
4846
+ if (scalarConf.path) {
4847
+ integrations["scalar"] = scalarConf.path;
4848
+ } else {
4849
+ const plugin = routers.find((r) => r.constructor.name === "ScalarPlugin");
4850
+ if (plugin) {
4851
+ integrations["scalar"] = plugin[$mountPath];
4852
+ }
4853
+ }
4854
+ }
4855
+ const asyncApiConf = checkConfig("asyncapi");
4856
+ if (asyncApiConf.enabled) {
4857
+ if (asyncApiConf.path) {
4858
+ integrations["asyncapi"] = asyncApiConf.path;
4859
+ } else {
4860
+ const plugin = routers.find((r) => r.constructor.name === "AsyncApiPlugin");
4861
+ if (plugin) {
4862
+ integrations["asyncapi"] = plugin[$mountPath];
4863
+ }
4864
+ }
4865
+ }
4866
+ return integrations;
4867
+ }
4868
+ // Get base path for dashboard files - works in both dev (src/) and production (dist/)
4869
+ static getBasePath() {
4870
+ const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
4871
+ if (dir.endsWith("dist")) {
4872
+ return dir + "/plugins/application/dashboard";
4873
+ }
4874
+ return dir;
4875
+ }
3829
4876
  setupRoutes() {
3830
4877
  this.router.get("/metrics", async (ctx) => {
3831
4878
  const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
@@ -3850,7 +4897,7 @@ class Dashboard {
3850
4897
  const startTime = Date.now() - ms;
3851
4898
  let stats;
3852
4899
  try {
3853
- stats = await datastore.query(`
4900
+ stats = await this.db.query(`
3854
4901
  SELECT
3855
4902
  count() as total,
3856
4903
  count(IF status < 400 THEN 1 END) as success,
@@ -3911,7 +4958,7 @@ class Dashboard {
3911
4958
  const periodMs = intervalMap[interval] || 60 * 1e3;
3912
4959
  const startTime = Date.now() - periodMs * 3;
3913
4960
  const endTime = Date.now();
3914
- const result = await datastore.query(
4961
+ const result = await this.db.query(
3915
4962
  "SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
3916
4963
  { start: startTime, end: endTime, interval }
3917
4964
  );
@@ -3940,7 +4987,7 @@ class Dashboard {
3940
4987
  };
3941
4988
  this.router.get("/requests/top", async (ctx) => {
3942
4989
  const startTime = getIntervalStartTime(ctx.query["interval"]);
3943
- const result = await datastore.query(
4990
+ const result = await this.db.query(
3944
4991
  "SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3945
4992
  { start: startTime }
3946
4993
  );
@@ -3948,7 +4995,7 @@ class Dashboard {
3948
4995
  });
3949
4996
  this.router.get("/errors/top", async (ctx) => {
3950
4997
  const startTime = getIntervalStartTime(ctx.query["interval"]);
3951
- const result = await datastore.query(
4998
+ const result = await this.db.query(
3952
4999
  "SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
3953
5000
  { start: startTime }
3954
5001
  );
@@ -3956,7 +5003,7 @@ class Dashboard {
3956
5003
  });
3957
5004
  this.router.get("/requests/failing", async (ctx) => {
3958
5005
  const startTime = getIntervalStartTime(ctx.query["interval"]);
3959
- const result = await datastore.query(
5006
+ const result = await this.db.query(
3960
5007
  "SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3961
5008
  { start: startTime }
3962
5009
  );
@@ -3964,7 +5011,7 @@ class Dashboard {
3964
5011
  });
3965
5012
  this.router.get("/requests/slowest", async (ctx) => {
3966
5013
  const startTime = getIntervalStartTime(ctx.query["interval"]);
3967
- const result = await datastore.query(
5014
+ const result = await this.db.query(
3968
5015
  "SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
3969
5016
  { start: startTime }
3970
5017
  );
@@ -3982,15 +5029,15 @@ class Dashboard {
3982
5029
  return ctx.json({ registry: registry || {} });
3983
5030
  });
3984
5031
  this.router.get("/requests", async (ctx) => {
3985
- const result = await datastore.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
5032
+ const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
3986
5033
  return ctx.json({ requests: result[0] || [] });
3987
5034
  });
3988
5035
  this.router.get("/requests/:id", async (ctx) => {
3989
- const result = await datastore.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
5036
+ const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
3990
5037
  return ctx.json({ request: result[0]?.[0] });
3991
5038
  });
3992
5039
  this.router.get("/failures", async (ctx) => {
3993
- const result = await datastore.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
5040
+ const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
3994
5041
  return ctx.json({ failures: result[0] });
3995
5042
  });
3996
5043
  this.router.post("/replay", async (ctx) => {
@@ -4014,18 +5061,51 @@ class Dashboard {
4014
5061
  return ctx.json({ error: String(e) }, 500);
4015
5062
  }
4016
5063
  });
4017
- this.router.get("/", async (ctx) => {
5064
+ this.router.get("/**", async (ctx) => {
5065
+ const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
5066
+ let relativePath = ctx.path;
5067
+ if (relativePath.startsWith(mountPath)) {
5068
+ relativePath = relativePath.slice(mountPath.length);
5069
+ }
5070
+ if (relativePath.startsWith("/")) {
5071
+ relativePath = relativePath.slice(1);
5072
+ }
5073
+ const path2 = relativePath;
5074
+ const staticFiles = [
5075
+ "charts.js",
5076
+ "failures.js",
5077
+ "graph.mjs",
5078
+ "poll.js",
5079
+ "reactflow.css",
5080
+ "registry.css",
5081
+ "registry.js",
5082
+ "requests.js",
5083
+ "styles.css",
5084
+ "tables.js",
5085
+ "tabs.js",
5086
+ "tabulator.css",
5087
+ "theme.css"
5088
+ ];
5089
+ if (staticFiles.includes(path2)) {
5090
+ const content = await promises.readFile(path$1.join(Dashboard.getBasePath(), "static", path2), "utf-8");
5091
+ if (path2.endsWith(".css")) ctx.set("Content-Type", "text/css");
5092
+ else if (path2.endsWith(".js") || path2.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
5093
+ return ctx.send(content);
5094
+ }
4018
5095
  const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
4019
5096
  const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
4020
- const linkPattern = this.getLinkPattern();
4021
- const template = await promises.readFile(Dashboard.getBasePath() + "/template.eta", "utf8");
4022
- return ctx.html(this.eta.renderString(template, {
5097
+ this.getLinkPattern();
5098
+ const integrations = this.detectIntegrations();
5099
+ const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
5100
+ const html = renderToString(DashboardApp({
4023
5101
  metrics: this.metrics,
4024
5102
  uptime,
4025
5103
  rootPath: process.cwd(),
4026
- linkPattern,
4027
- headers: this.dashboardConfig.getRequestHeaders?.()
5104
+ integrations,
5105
+ base: mountPath,
5106
+ getRequestHeadersSource
4028
5107
  }));
5108
+ return ctx.html(`<!DOCTYPE html>${html}`);
4029
5109
  });
4030
5110
  }
4031
5111
  instrumentApp(app) {
@@ -4121,7 +5201,7 @@ class Dashboard {
4121
5201
  headers[k] = v;
4122
5202
  });
4123
5203
  }
4124
- await datastore.set(new surrealdb.RecordId("failed_requests", ctx.requestId), {
5204
+ await this.db.upsert(new surrealdb.RecordId("failed_requests", ctx.requestId), {
4125
5205
  method: ctx.method,
4126
5206
  url: ctx.url.toString(),
4127
5207
  headers,
@@ -4146,7 +5226,7 @@ class Dashboard {
4146
5226
  };
4147
5227
  this.metrics.logs.push(logEntry);
4148
5228
  try {
4149
- await datastore.set(new surrealdb.RecordId("requests", ctx.requestId), logEntry);
5229
+ await this.db.upsert(new surrealdb.RecordId("requests", ctx.requestId), logEntry);
4150
5230
  } catch (e) {
4151
5231
  console.error("Failed to record request log", e);
4152
5232
  }
@@ -4174,32 +5254,169 @@ class Dashboard {
4174
5254
  function unknownError(ctx) {
4175
5255
  return ctx.json({ error: "Unknown Error" }, 500);
4176
5256
  }
4177
- const eta = new eta$2.Eta();
5257
+ class GraphQLApolloPlugin extends ShokupanRouter {
5258
+ // Use generic any or verify type
5259
+ constructor(pluginOptions) {
5260
+ super();
5261
+ this.pluginOptions = pluginOptions;
5262
+ this.pluginOptions.path ??= "/graphql";
5263
+ }
5264
+ apolloServer;
5265
+ async onInit(app, options) {
5266
+ const { ApolloServer, HeaderMap } = await import("@apollo/server");
5267
+ this.apolloServer = new ApolloServer({
5268
+ typeDefs: this.pluginOptions.typeDefs,
5269
+ resolvers: this.pluginOptions.resolvers,
5270
+ ...this.pluginOptions.apolloConfig || {}
5271
+ });
5272
+ const path2 = options?.path || this.pluginOptions.path || "/graphql";
5273
+ app.mount(path2, this);
5274
+ app.onStart(async () => {
5275
+ await this.apolloServer.start();
5276
+ });
5277
+ this.post("/", async (ctx) => {
5278
+ const body = await ctx.body();
5279
+ const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
5280
+ httpGraphQLRequest: {
5281
+ body,
5282
+ method: ctx.req.method,
5283
+ search: ctx.url.search,
5284
+ headers: new HeaderMap(ctx.req.headers)
5285
+ },
5286
+ // Pass the Shokupan Context as the GraphQL Context
5287
+ context: async () => ({ ...ctx, shokupan: ctx })
5288
+ });
5289
+ for (const [key, value] of httpGraphQLResponse.headers) {
5290
+ ctx.set(key, value);
5291
+ }
5292
+ if (httpGraphQLResponse.body.kind === "complete") {
5293
+ return ctx.send(httpGraphQLResponse.body.string, {
5294
+ status: httpGraphQLResponse.status ?? 200
5295
+ });
5296
+ } else {
5297
+ let string = "";
5298
+ for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
5299
+ string += chunk;
5300
+ }
5301
+ return ctx.send(string, {
5302
+ status: httpGraphQLResponse.status ?? 200
5303
+ });
5304
+ }
5305
+ });
5306
+ this.get("/", async (ctx) => {
5307
+ const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
5308
+ httpGraphQLRequest: {
5309
+ body: Object.keys(ctx.query).length > 0 ? ctx.query : void 0,
5310
+ method: ctx.req.method,
5311
+ search: ctx.url.search,
5312
+ headers: new HeaderMap(ctx.req.headers)
5313
+ },
5314
+ context: async () => ({ ...ctx, shokupan: ctx })
5315
+ });
5316
+ for (const [key, value] of httpGraphQLResponse.headers) {
5317
+ ctx.set(key, value);
5318
+ }
5319
+ if (httpGraphQLResponse.body.kind === "complete") {
5320
+ return ctx.html(httpGraphQLResponse.body.string, httpGraphQLResponse.status ?? 200);
5321
+ } else {
5322
+ let string = "";
5323
+ for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
5324
+ string += chunk;
5325
+ }
5326
+ return ctx.html(string, httpGraphQLResponse.status ?? 200);
5327
+ }
5328
+ });
5329
+ }
5330
+ }
4178
5331
  class ScalarPlugin extends ShokupanRouter {
4179
5332
  constructor(pluginOptions = {}) {
4180
5333
  pluginOptions.config ??= {};
4181
5334
  super();
4182
5335
  this.pluginOptions = pluginOptions;
4183
- this.init();
5336
+ this.initRoutes();
5337
+ }
5338
+ eta;
5339
+ async onInit(app, options) {
5340
+ const { Eta } = await import("eta");
5341
+ this.eta = new Eta();
5342
+ const path2 = options?.path || this.pluginOptions.path || "/reference";
5343
+ app.mount(path2, this);
5344
+ this.onMount(app);
4184
5345
  }
4185
- onInit(app, options) {
4186
- if (options?.path) {
4187
- app.mount(options.path, this);
4188
- } else {
4189
- app.mount(options.path ?? "/", this);
5346
+ async ensureEta() {
5347
+ if (!this.eta) {
5348
+ const { Eta } = await import("eta");
5349
+ this.eta = new Eta();
4190
5350
  }
4191
- this.onMount(app);
4192
5351
  }
4193
- init() {
4194
- this.get("/", (ctx) => {
5352
+ initRoutes() {
5353
+ const bootId = Date.now().toString();
5354
+ this.get("/_lifecycle", (ctx) => ctx.json({ boot: bootId }));
5355
+ this.get("/", async (ctx) => {
5356
+ await this.ensureEta();
4195
5357
  let path2 = ctx.url.toString();
4196
5358
  if (!path2.endsWith("/")) path2 += "/";
4197
- return ctx.html(eta.renderString(`<!doctype html>
4198
- <html>
5359
+ const devScript = ctx.app?.applicationConfig.development ? `
5360
+ <script>
5361
+ (function() {
5362
+ const bootId = "${bootId}";
5363
+ let isDown = false;
5364
+
5365
+ setInterval(async () => {
5366
+ try {
5367
+ const res = await fetch('${path2}_lifecycle');
5368
+ if (!res.ok) throw new Error('Down');
5369
+ const data = await res.json();
5370
+ if (data.boot !== bootId) {
5371
+ console.log('Server restarted, reloading...');
5372
+ window.location.reload();
5373
+ }
5374
+ else if (isDown) {
5375
+ isDown = false;
5376
+ }
5377
+ } catch (e) {
5378
+ isDown = true;
5379
+ console.log('Connection lost...');
5380
+ }
5381
+ }, 2000);
5382
+ })();
5383
+ <\/script>
5384
+ ` : "";
5385
+ let themeCss = "";
5386
+ try {
5387
+ try {
5388
+ themeCss = fs.readFileSync(path$1.join(process.cwd(), "src/theme.css"), "utf-8");
5389
+ } catch {
5390
+ }
5391
+ } catch (e) {
5392
+ }
5393
+ if (!this.eta) throw new Error("Eta not initialized");
5394
+ return ctx.html(this.eta.renderString(`<!doctype html>
5395
+ <html lang="en">
4199
5396
  <head>
4200
5397
  <title>API Reference</title>
4201
5398
  <meta charset = "utf-8" />
4202
5399
  <meta name="viewport" content = "width=device-width, initial-scale=1" />
5400
+ <style>
5401
+ ${themeCss}
5402
+
5403
+ :root {
5404
+ --scalar-color-1: var(--primary);
5405
+ --scalar-color-2: var(--secondary);
5406
+ --scalar-color-3: var(--accent);
5407
+ --scalar-color-accent: var(--accent);
5408
+
5409
+ --scalar-background-1: var(--bg-primary);
5410
+ --scalar-background-2: var(--bg-secondary);
5411
+ --scalar-background-3: var(--bg-card);
5412
+
5413
+ --scalar-text-1: var(--text-primary);
5414
+ --scalar-text-2: var(--text-secondary);
5415
+ --scalar-text-3: var(--text-muted);
5416
+
5417
+ --scalar-border-color: var(--border-color);
5418
+ }
5419
+ </style>
4203
5420
  </head>
4204
5421
 
4205
5422
  <body>
@@ -4211,9 +5428,10 @@ class ScalarPlugin extends ShokupanRouter {
4211
5428
  }
4212
5429
  ])
4213
5430
  <\/script>
5431
+ <%~ it.devScript %>
4214
5432
  </body>
4215
5433
 
4216
- </html>`, { path: path2, config: this.pluginOptions }));
5434
+ </html>`, { path: path2, config: this.pluginOptions, devScript }));
4217
5435
  });
4218
5436
  this.get("/openapi.json", async (ctx) => {
4219
5437
  let spec;
@@ -4416,7 +5634,7 @@ function Cors(options = {}) {
4416
5634
  }
4417
5635
  const response = await next();
4418
5636
  if (response instanceof Response) {
4419
- const headerEntries = Array.from(headers.entries());
5637
+ const headerEntries = Object.entries(headers);
4420
5638
  for (let i = 0; i < headerEntries.length; i++) {
4421
5639
  const [key, value] = headerEntries[i];
4422
5640
  response.headers.set(key, value);
@@ -5187,6 +6405,7 @@ exports.$socket = $socket;
5187
6405
  exports.$url = $url;
5188
6406
  exports.$ws = $ws;
5189
6407
  exports.All = All;
6408
+ exports.AsyncApiPlugin = AsyncApiPlugin;
5190
6409
  exports.AuthPlugin = AuthPlugin;
5191
6410
  exports.Body = Body;
5192
6411
  exports.ClusterPlugin = ClusterPlugin;
@@ -5199,6 +6418,7 @@ exports.Dashboard = Dashboard;
5199
6418
  exports.Delete = Delete;
5200
6419
  exports.Event = Event;
5201
6420
  exports.Get = Get;
6421
+ exports.GraphQLApolloPlugin = GraphQLApolloPlugin;
5202
6422
  exports.HTTPMethods = HTTPMethods;
5203
6423
  exports.Head = Head;
5204
6424
  exports.Headers = Headers$1;