shokupan 0.6.1 → 0.9.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 (81) hide show
  1. package/README.md +55 -2
  2. package/dist/{openapi-analyzer-Bei1sVWp.cjs → analyzer-Bei1sVWp.cjs} +1 -1
  3. package/dist/analyzer-Bei1sVWp.cjs.map +1 -0
  4. package/dist/{openapi-analyzer-Ce_7JxZh.js → analyzer-Ce_7JxZh.js} +1 -1
  5. package/dist/analyzer-Ce_7JxZh.js.map +1 -0
  6. package/dist/cli.cjs +2 -2
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/cli.js.map +1 -1
  10. package/dist/context.d.ts +58 -23
  11. package/dist/{server-adapter-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
  12. package/dist/http-server-BEMPIs33.cjs.map +1 -0
  13. package/dist/{server-adapter-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
  14. package/dist/http-server-CCeagTyU.js.map +1 -0
  15. package/dist/index.cjs +1940 -917
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.ts +18 -17
  18. package/dist/index.js +1948 -925
  19. package/dist/index.js.map +1 -1
  20. package/dist/middleware.d.ts +1 -1
  21. package/dist/plugins/{auth.d.ts → application/auth.d.ts} +72 -3
  22. package/dist/plugins/application/cluster.d.ts +33 -0
  23. package/dist/plugins/{failed-request-recorder.d.ts → application/dashboard/failed-request-recorder.d.ts} +1 -1
  24. package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
  25. package/dist/plugins/application/dashboard/plugin.d.ts +42 -0
  26. package/dist/plugins/application/dashboard/static/charts.js +328 -0
  27. package/dist/plugins/application/dashboard/static/failures.js +85 -0
  28. package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
  29. package/dist/plugins/application/dashboard/static/poll.js +146 -0
  30. package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
  31. package/dist/plugins/application/dashboard/static/registry.css +131 -0
  32. package/dist/plugins/application/dashboard/static/registry.js +269 -0
  33. package/dist/plugins/application/dashboard/static/requests.js +118 -0
  34. package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
  35. package/dist/plugins/application/dashboard/static/styles.css +175 -0
  36. package/dist/plugins/application/dashboard/static/tables.js +92 -0
  37. package/dist/plugins/application/dashboard/static/tabs.js +113 -0
  38. package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
  39. package/dist/plugins/application/dashboard/template.eta +246 -0
  40. package/dist/plugins/{server-adapter.d.ts → application/http-server.d.ts} +1 -1
  41. package/dist/plugins/{idempotency → application/idempotency}/plugin.d.ts +7 -1
  42. package/dist/plugins/{openapi.d.ts → application/openapi/openapi.d.ts} +2 -2
  43. package/dist/plugins/application/scalar.d.ts +36 -0
  44. package/dist/plugins/application/socket-io.d.ts +14 -0
  45. package/dist/plugins/middleware/compression.d.ts +17 -0
  46. package/dist/plugins/middleware/cors.d.ts +34 -0
  47. package/dist/plugins/{express.d.ts → middleware/express.d.ts} +1 -1
  48. package/dist/plugins/{openapi-validator.d.ts → middleware/openapi-validator.d.ts} +2 -2
  49. package/dist/plugins/middleware/proxy.d.ts +37 -0
  50. package/dist/plugins/middleware/rate-limit.d.ts +58 -0
  51. package/dist/plugins/{security-headers.d.ts → middleware/security-headers.d.ts} +51 -1
  52. package/dist/plugins/{serve-static.d.ts → middleware/serve-static.d.ts} +1 -1
  53. package/dist/plugins/{session.d.ts → middleware/session.d.ts} +89 -3
  54. package/dist/plugins/{validation.d.ts → middleware/validation.d.ts} +6 -1
  55. package/dist/router.d.ts +17 -5
  56. package/dist/shokupan.d.ts +31 -5
  57. package/dist/util/async-hooks.d.ts +8 -2
  58. package/dist/util/datastore.d.ts +4 -3
  59. package/dist/{decorators.d.ts → util/decorators.d.ts} +6 -1
  60. package/dist/util/http-error.d.ts +38 -0
  61. package/dist/util/http-status.d.ts +32 -0
  62. package/dist/util/instrumentation.d.ts +1 -1
  63. package/dist/{request.d.ts → util/request.d.ts} +1 -1
  64. package/dist/util/symbol.d.ts +34 -0
  65. package/dist/{router → util}/trie.d.ts +1 -1
  66. package/dist/{types.d.ts → util/types.d.ts} +38 -2
  67. package/package.json +9 -6
  68. package/dist/openapi-analyzer-Bei1sVWp.cjs.map +0 -1
  69. package/dist/openapi-analyzer-Ce_7JxZh.js.map +0 -1
  70. package/dist/plugins/compression.d.ts +0 -5
  71. package/dist/plugins/cors.d.ts +0 -11
  72. package/dist/plugins/debugview/plugin.d.ts +0 -29
  73. package/dist/plugins/proxy.d.ts +0 -11
  74. package/dist/plugins/rate-limit.d.ts +0 -15
  75. package/dist/plugins/scalar.d.ts +0 -15
  76. package/dist/server-adapter-0xH174zz.js.map +0 -1
  77. package/dist/server-adapter-DFhwlK8e.cjs.map +0 -1
  78. package/dist/symbol.d.ts +0 -15
  79. /package/dist/{analysis/openapi-analyzer.d.ts → plugins/application/openapi/analyzer.d.ts} +0 -0
  80. /package/dist/{di.d.ts → util/di.d.ts} +0 -0
  81. /package/dist/{response.d.ts → util/response.d.ts} +0 -0
package/dist/index.cjs CHANGED
@@ -22,21 +22,29 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
25
+ const nanoid = require("nanoid");
25
26
  const promises = require("node:fs/promises");
27
+ const api = require("@opentelemetry/api");
28
+ const node_async_hooks = require("node:async_hooks");
29
+ const surrealdb = require("surrealdb");
26
30
  const eta$2 = require("eta");
27
31
  const promises$1 = require("fs/promises");
28
32
  const path = require("path");
29
- const node_async_hooks = require("node:async_hooks");
30
- const api = require("@opentelemetry/api");
31
33
  const os = require("node:os");
32
34
  const arctic = require("arctic");
33
35
  const jose = require("jose");
36
+ const cluster = require("node:cluster");
37
+ const net = require("node:net");
38
+ const path$1 = require("node:path");
39
+ const node_url = require("node:url");
40
+ const node_perf_hooks = require("node:perf_hooks");
41
+ const analyzer = require("./analyzer-Bei1sVWp.cjs");
34
42
  const zlib = require("node:zlib");
35
43
  const Ajv = require("ajv");
36
44
  const addFormats = require("ajv-formats");
37
- const openapiAnalyzer = require("./openapi-analyzer-Bei1sVWp.cjs");
38
45
  const crypto = require("crypto");
39
46
  const events = require("events");
47
+ var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
40
48
  function _interopNamespaceDefault(e) {
41
49
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
42
50
  if (e) {
@@ -56,6 +64,102 @@ function _interopNamespaceDefault(e) {
56
64
  const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
57
65
  const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
58
66
  const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
67
+ const HTTP_STATUS = {
68
+ // 2xx Success
69
+ OK: 200,
70
+ CREATED: 201,
71
+ ACCEPTED: 202,
72
+ NO_CONTENT: 204,
73
+ // 3xx Redirection
74
+ MOVED_PERMANENTLY: 301,
75
+ FOUND: 302,
76
+ SEE_OTHER: 303,
77
+ NOT_MODIFIED: 304,
78
+ TEMPORARY_REDIRECT: 307,
79
+ PERMANENT_REDIRECT: 308,
80
+ // 4xx Client Errors
81
+ BAD_REQUEST: 400,
82
+ UNAUTHORIZED: 401,
83
+ FORBIDDEN: 403,
84
+ NOT_FOUND: 404,
85
+ METHOD_NOT_ALLOWED: 405,
86
+ REQUEST_TIMEOUT: 408,
87
+ CONFLICT: 409,
88
+ UNPROCESSABLE_ENTITY: 422,
89
+ TOO_MANY_REQUESTS: 429,
90
+ // 5xx Server Errors
91
+ INTERNAL_SERVER_ERROR: 500,
92
+ NOT_IMPLEMENTED: 501,
93
+ BAD_GATEWAY: 502,
94
+ SERVICE_UNAVAILABLE: 503,
95
+ GATEWAY_TIMEOUT: 504
96
+ };
97
+ const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
98
+ 100,
99
+ 101,
100
+ 102,
101
+ 103,
102
+ 200,
103
+ 201,
104
+ 202,
105
+ 203,
106
+ 204,
107
+ 205,
108
+ 206,
109
+ 207,
110
+ 208,
111
+ 226,
112
+ 300,
113
+ 301,
114
+ 302,
115
+ 303,
116
+ 304,
117
+ 305,
118
+ 306,
119
+ 307,
120
+ 308,
121
+ 400,
122
+ 401,
123
+ 402,
124
+ 403,
125
+ 404,
126
+ 405,
127
+ 406,
128
+ 407,
129
+ 408,
130
+ 409,
131
+ 410,
132
+ 411,
133
+ 412,
134
+ 413,
135
+ 414,
136
+ 415,
137
+ 416,
138
+ 417,
139
+ 418,
140
+ 421,
141
+ 422,
142
+ 423,
143
+ 424,
144
+ 425,
145
+ 426,
146
+ 428,
147
+ 429,
148
+ 431,
149
+ 451,
150
+ 500,
151
+ 501,
152
+ 502,
153
+ 503,
154
+ 504,
155
+ 505,
156
+ 506,
157
+ 507,
158
+ 508,
159
+ 510,
160
+ 511
161
+ ]);
162
+ const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
59
163
  class ShokupanResponse {
60
164
  _headers = null;
61
165
  _status = 200;
@@ -119,6 +223,40 @@ class ShokupanResponse {
119
223
  return this._headers !== null;
120
224
  }
121
225
  }
226
+ const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
227
+ const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
228
+ const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
229
+ const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
230
+ const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
231
+ const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
232
+ const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
233
+ const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
234
+ const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
235
+ const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
236
+ const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
237
+ const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
238
+ const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
239
+ const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
240
+ const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
241
+ const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
242
+ const $url = /* @__PURE__ */ Symbol.for("Shokupan.ctx.url");
243
+ const $requestId = /* @__PURE__ */ Symbol.for("Shokupan.ctx.requestId");
244
+ const $debug = /* @__PURE__ */ Symbol.for("Shokupan.ctx.debug");
245
+ const $finalResponse = /* @__PURE__ */ Symbol.for("Shokupan.ctx.finalResponse");
246
+ const $rawBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.rawBody");
247
+ const $cachedBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedBody");
248
+ const $bodyType = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyType");
249
+ const $bodyParsed = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParsed");
250
+ const $bodyParseError = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParseError");
251
+ const $routeMatched = /* @__PURE__ */ Symbol.for("Shokupan.ctx.routeMatched");
252
+ const $cachedHostname = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHostname");
253
+ const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol");
254
+ const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
255
+ const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
256
+ const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
257
+ const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
258
+ const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
259
+ const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
122
260
  function isValidCookieDomain(domain, currentHost) {
123
261
  const hostWithoutPort = currentHost.split(":")[0];
124
262
  if (domain === hostWithoutPort) return true;
@@ -128,72 +266,6 @@ function isValidCookieDomain(domain, currentHost) {
128
266
  }
129
267
  return false;
130
268
  }
131
- const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
132
- 100,
133
- 101,
134
- 102,
135
- 103,
136
- 200,
137
- 201,
138
- 202,
139
- 203,
140
- 204,
141
- 205,
142
- 206,
143
- 207,
144
- 208,
145
- 226,
146
- 300,
147
- 301,
148
- 302,
149
- 303,
150
- 304,
151
- 305,
152
- 306,
153
- 307,
154
- 308,
155
- 400,
156
- 401,
157
- 402,
158
- 403,
159
- 404,
160
- 405,
161
- 406,
162
- 407,
163
- 408,
164
- 409,
165
- 410,
166
- 411,
167
- 412,
168
- 413,
169
- 414,
170
- 415,
171
- 416,
172
- 417,
173
- 418,
174
- 421,
175
- 422,
176
- 423,
177
- 424,
178
- 425,
179
- 426,
180
- 428,
181
- 429,
182
- 431,
183
- 451,
184
- 500,
185
- 501,
186
- 502,
187
- 503,
188
- 504,
189
- 505,
190
- 506,
191
- 507,
192
- 508,
193
- 510,
194
- 511
195
- ]);
196
- const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
197
269
  class ShokupanContext {
198
270
  constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
199
271
  this.request = request;
@@ -222,28 +294,43 @@ class ShokupanContext {
222
294
  state;
223
295
  handlerStack = [];
224
296
  response;
225
- _debug;
226
- _finalResponse;
227
- _rawBody;
297
+ [$debug];
298
+ [$finalResponse];
299
+ [$rawBody];
228
300
  // Raw body for compression optimization
229
301
  // Body caching to avoid double parsing
230
- _url;
231
- _cachedBody;
232
- _bodyType;
233
- _bodyParsed = false;
234
- _bodyParseError;
302
+ [$url];
303
+ [$cachedBody];
304
+ [$bodyType];
305
+ [$bodyParsed] = false;
306
+ [$bodyParseError];
307
+ [$routeMatched] = false;
235
308
  // Cached URL properties to avoid repeated parsing
236
- _cachedHostname;
237
- _cachedProtocol;
238
- _cachedHost;
239
- _cachedOrigin;
240
- _cachedQuery;
309
+ [$cachedHostname];
310
+ [$cachedProtocol];
311
+ [$cachedHost];
312
+ [$cachedOrigin];
313
+ [$cachedQuery];
314
+ [$ws];
315
+ [$socket];
316
+ [$io];
317
+ /**
318
+ * JSX Rendering Function
319
+ */
320
+ renderer;
321
+ setRenderer(renderer) {
322
+ this.renderer = renderer;
323
+ }
324
+ [$requestId];
325
+ get requestId() {
326
+ return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid.nanoid();
327
+ }
241
328
  get url() {
242
- if (!this._url) {
329
+ if (!this[$url]) {
243
330
  const urlString = this.request.url || "http://localhost/";
244
- this._url = new URL(urlString);
331
+ this[$url] = new URL(urlString);
245
332
  }
246
- return this._url;
333
+ return this[$url];
247
334
  }
248
335
  /**
249
336
  * Base request
@@ -261,7 +348,7 @@ class ShokupanContext {
261
348
  * Request path
262
349
  */
263
350
  get path() {
264
- if (this._url) return this._url.pathname;
351
+ if (this[$url]) return this[$url].pathname;
265
352
  const url = this.request.url;
266
353
  let queryIndex = url.indexOf("?");
267
354
  const end = queryIndex === -1 ? url.length : queryIndex;
@@ -286,7 +373,7 @@ class ShokupanContext {
286
373
  * Request query params
287
374
  */
288
375
  get query() {
289
- if (this._cachedQuery) return this._cachedQuery;
376
+ if (this[$cachedQuery]) return this[$cachedQuery];
290
377
  const q = /* @__PURE__ */ Object.create(null);
291
378
  const blocklist = ["__proto__", "constructor", "prototype"];
292
379
  const entries = Object.entries(this.url.searchParams);
@@ -303,7 +390,7 @@ class ShokupanContext {
303
390
  q[key] = value;
304
391
  }
305
392
  }
306
- this._cachedQuery = q;
393
+ this[$cachedQuery] = q;
307
394
  return q;
308
395
  }
309
396
  /**
@@ -316,19 +403,19 @@ class ShokupanContext {
316
403
  * Request hostname (e.g. "localhost")
317
404
  */
318
405
  get hostname() {
319
- return this._cachedHostname ??= this.url.hostname;
406
+ return this[$cachedHostname] ??= this.url.hostname;
320
407
  }
321
408
  /**
322
409
  * Request host (e.g. "localhost:3000")
323
410
  */
324
411
  get host() {
325
- return this._cachedHost ??= this.url.host;
412
+ return this[$cachedHost] ??= this.url.host;
326
413
  }
327
414
  /**
328
415
  * Request protocol (e.g. "http:", "https:")
329
416
  */
330
417
  get protocol() {
331
- return this._cachedProtocol ??= this.url.protocol;
418
+ return this[$cachedProtocol] ??= this.url.protocol;
332
419
  }
333
420
  /**
334
421
  * Whether request is secure (https)
@@ -340,7 +427,7 @@ class ShokupanContext {
340
427
  * Request origin (e.g. "http://localhost:3000")
341
428
  */
342
429
  get origin() {
343
- return this._cachedOrigin ??= this.url.origin;
430
+ return this[$cachedOrigin] ??= this.url.origin;
344
431
  }
345
432
  /**
346
433
  * Request headers
@@ -361,6 +448,24 @@ class ShokupanContext {
361
448
  get res() {
362
449
  return this.response;
363
450
  }
451
+ /**
452
+ * Raw WebSocket connection
453
+ */
454
+ get ws() {
455
+ return this[$ws];
456
+ }
457
+ /**
458
+ * Socket.io socket
459
+ */
460
+ get socket() {
461
+ return this[$socket];
462
+ }
463
+ /**
464
+ * Socket.io server
465
+ */
466
+ get io() {
467
+ return this[$io];
468
+ }
364
469
  /**
365
470
  * Helper to set a header on the response
366
471
  * @param key Header key
@@ -370,6 +475,20 @@ class ShokupanContext {
370
475
  this.response.set(key, value);
371
476
  return this;
372
477
  }
478
+ isUpgraded = false;
479
+ /**
480
+ * Upgrades the request to a WebSocket connection.
481
+ * @param options Upgrade options
482
+ * @returns true if upgraded, false otherwise
483
+ */
484
+ upgrade(options) {
485
+ if (!this.server) return false;
486
+ const success = this.server.upgrade(this.req, options);
487
+ if (success) {
488
+ this.isUpgraded = true;
489
+ }
490
+ return success;
491
+ }
373
492
  /**
374
493
  * Set a cookie
375
494
  * @param name Cookie name
@@ -447,33 +566,37 @@ class ShokupanContext {
447
566
  * The body is only parsed once and cached for subsequent reads.
448
567
  */
449
568
  async body() {
450
- if (this._bodyParseError) {
451
- throw this._bodyParseError;
569
+ if (this[$bodyParseError]) {
570
+ throw this[$bodyParseError];
452
571
  }
453
- if (this._bodyParsed) {
454
- return this._cachedBody;
572
+ if (this[$bodyParsed]) {
573
+ return this[$cachedBody];
455
574
  }
456
575
  const contentType = this.request.headers.get("content-type") || "";
457
576
  if (contentType.includes("application/json") || contentType.includes("+json")) {
458
- const rawText = await this.readRawBody();
459
577
  const parserType = this.app?.applicationConfig?.jsonParser || "native";
460
578
  if (parserType === "native") {
461
- this._cachedBody = JSON.parse(rawText);
579
+ try {
580
+ this[$cachedBody] = await this.request.json();
581
+ } catch (e) {
582
+ throw e;
583
+ }
462
584
  } else {
585
+ const rawText = await this.request.text();
463
586
  const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
464
587
  const parser = getJSONParser(parserType);
465
- this._cachedBody = parser(rawText);
588
+ this[$cachedBody] = parser(rawText);
466
589
  }
467
- this._bodyType = "json";
590
+ this[$bodyType] = "json";
468
591
  } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
469
- this._cachedBody = await this.request.formData();
470
- this._bodyType = "formData";
592
+ this[$cachedBody] = await this.request.formData();
593
+ this[$bodyType] = "formData";
471
594
  } else {
472
- this._cachedBody = await this.readRawBody();
473
- this._bodyType = "text";
595
+ this[$cachedBody] = await this.request.text();
596
+ this[$bodyType] = "text";
474
597
  }
475
- this._bodyParsed = true;
476
- return this._cachedBody;
598
+ this[$bodyParsed] = true;
599
+ return this[$cachedBody];
477
600
  }
478
601
  /**
479
602
  * Pre-parse the request body before handler execution.
@@ -481,7 +604,7 @@ class ShokupanContext {
481
604
  * Errors are deferred until the body is actually accessed in the handler.
482
605
  */
483
606
  async parseBody() {
484
- if (this._bodyParsed) {
607
+ if (this[$bodyParsed]) {
485
608
  return;
486
609
  }
487
610
  if (this.request.method === "GET" || this.request.method === "HEAD") {
@@ -490,7 +613,7 @@ class ShokupanContext {
490
613
  try {
491
614
  await this.body();
492
615
  } catch (error) {
493
- this._bodyParseError = error;
616
+ this[$bodyParseError] = error;
494
617
  }
495
618
  }
496
619
  /**
@@ -540,10 +663,21 @@ class ShokupanContext {
540
663
  throw new Error(`Invalid HTTP status code: ${status}`);
541
664
  }
542
665
  if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
543
- this._rawBody = body;
666
+ this[$rawBody] = body;
667
+ }
668
+ return this[$finalResponse] ??= new Response(body, { status, headers });
669
+ }
670
+ /**
671
+ * Emit an event to the client (WebSocket only)
672
+ * @param event Event name
673
+ * @param data Event data (Must be JSON serializable)
674
+ */
675
+ emit(event, data) {
676
+ if (this[$ws]) {
677
+ this[$ws].send(JSON.stringify({ event, data }));
678
+ } else if (this[$socket]) {
679
+ this[$socket].emit(event, data);
544
680
  }
545
- this._finalResponse = new Response(body, { status, headers });
546
- return this._finalResponse;
547
681
  }
548
682
  /**
549
683
  * Respond with a JSON object
@@ -554,18 +688,18 @@ class ShokupanContext {
554
688
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
555
689
  }
556
690
  const jsonString = JSON.stringify(data);
557
- this._rawBody = jsonString;
691
+ this[$rawBody] = jsonString;
558
692
  if (!headers && !this.response.hasPopulatedHeaders) {
559
- this._finalResponse = new Response(jsonString, {
693
+ this[$finalResponse] = new Response(jsonString, {
560
694
  status: finalStatus,
561
695
  headers: { "content-type": "application/json" }
562
696
  });
563
- return this._finalResponse;
697
+ return this[$finalResponse];
564
698
  }
565
699
  const finalHeaders = this.mergeHeaders(headers);
566
700
  finalHeaders.set("content-type", "application/json");
567
- this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
568
- return this._finalResponse;
701
+ this[$finalResponse] = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
702
+ return this[$finalResponse];
569
703
  }
570
704
  /**
571
705
  * Respond with a text string
@@ -575,18 +709,18 @@ class ShokupanContext {
575
709
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
576
710
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
577
711
  }
578
- this._rawBody = data;
712
+ this[$rawBody] = data;
579
713
  if (!headers && !this.response.hasPopulatedHeaders) {
580
- this._finalResponse = new Response(data, {
714
+ this[$finalResponse] = new Response(data, {
581
715
  status: finalStatus,
582
716
  headers: { "content-type": "text/plain; charset=utf-8" }
583
717
  });
584
- return this._finalResponse;
718
+ return this[$finalResponse];
585
719
  }
586
720
  const finalHeaders = this.mergeHeaders(headers);
587
721
  finalHeaders.set("content-type", "text/plain; charset=utf-8");
588
- this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
589
- return this._finalResponse;
722
+ this[$finalResponse] = new Response(data, { status: finalStatus, headers: finalHeaders });
723
+ return this[$finalResponse];
590
724
  }
591
725
  /**
592
726
  * Respond with HTML content
@@ -598,9 +732,9 @@ class ShokupanContext {
598
732
  }
599
733
  const finalHeaders = this.mergeHeaders(headers);
600
734
  finalHeaders.set("content-type", "text/html; charset=utf-8");
601
- this._rawBody = html;
602
- this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
603
- return this._finalResponse;
735
+ this[$rawBody] = html;
736
+ this[$finalResponse] = new Response(html, { status: finalStatus, headers: finalHeaders });
737
+ return this[$finalResponse];
604
738
  }
605
739
  /**
606
740
  * Respond with a redirect
@@ -611,8 +745,8 @@ class ShokupanContext {
611
745
  }
612
746
  const headers = this.mergeHeaders();
613
747
  headers.set("Location", url);
614
- this._finalResponse = new Response(null, { status, headers });
615
- return this._finalResponse;
748
+ this[$finalResponse] = new Response(null, { status, headers });
749
+ return this[$finalResponse];
616
750
  }
617
751
  /**
618
752
  * Respond with a status code
@@ -623,8 +757,8 @@ class ShokupanContext {
623
757
  throw new Error(`Invalid HTTP status code: ${status}`);
624
758
  }
625
759
  const headers = this.mergeHeaders();
626
- this._finalResponse = new Response(null, { status, headers });
627
- return this._finalResponse;
760
+ this[$finalResponse] = new Response(null, { status, headers });
761
+ return this[$finalResponse];
628
762
  }
629
763
  /**
630
764
  * Respond with a file
@@ -636,21 +770,17 @@ class ShokupanContext {
636
770
  throw new Error(`Invalid HTTP status code: ${status}`);
637
771
  }
638
772
  if (typeof Bun !== "undefined") {
639
- this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
640
- return this._finalResponse;
773
+ this[$finalResponse] = new Response(Bun.file(path2, fileOptions), { status, headers });
774
+ return this[$finalResponse];
641
775
  } else {
642
776
  const fileBuffer = await promises.readFile(path2);
643
777
  if (fileOptions?.type) {
644
778
  headers.set("content-type", fileOptions.type);
645
779
  }
646
- this._finalResponse = new Response(fileBuffer, { status, headers });
647
- return this._finalResponse;
780
+ this[$finalResponse] = new Response(fileBuffer, { status, headers });
781
+ return this[$finalResponse];
648
782
  }
649
783
  }
650
- /**
651
- * JSX Rendering Function
652
- */
653
- renderer;
654
784
  /**
655
785
  * Render a JSX element
656
786
  * @param element JSX Element
@@ -669,284 +799,67 @@ class ShokupanContext {
669
799
  return this.html(html, status, headers);
670
800
  }
671
801
  }
672
- function RateLimitMiddleware(options = {}) {
673
- const windowMs = options.windowMs || 60 * 1e3;
674
- const max = options.limit || options.max || 5;
675
- const message = options.message || "Too many requests, please try again later.";
676
- const statusCode = options.statusCode || 429;
677
- const headers = options.headers !== false;
678
- const mode = options.mode || "user";
679
- const trustedProxies = options.trustedProxies || [];
680
- const keyGenerator = options.keyGenerator || ((ctx) => {
681
- if (mode === "absolute") {
682
- return "global";
683
- }
684
- const xForwardedFor = ctx.headers.get("x-forwarded-for");
685
- if (xForwardedFor && trustedProxies.length > 0) {
686
- const ips = xForwardedFor.split(",").map((ip) => ip.trim());
687
- for (let i = ips.length - 1; i >= 0; i--) {
688
- const ip = ips[i];
689
- if (!trustedProxies.includes(ip)) {
690
- if (/^[\d.:a-fA-F]+$/.test(ip)) {
691
- return ip;
692
- }
693
- }
802
+ const compose = (middleware) => {
803
+ if (!middleware.length) {
804
+ return (context, next) => {
805
+ return next ? next() : Promise.resolve();
806
+ };
807
+ }
808
+ return function dispatch(context, next) {
809
+ let index = -1;
810
+ async function runner(i) {
811
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
812
+ index = i;
813
+ if (i >= middleware.length) {
814
+ return next ? next() : Promise.resolve();
694
815
  }
695
- }
696
- return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
697
- });
698
- const skip = options.skip || (() => false);
699
- const hits = /* @__PURE__ */ new Map();
700
- const interval = setInterval(() => {
701
- const now = Date.now();
702
- const entries = Array.from(hits.entries());
703
- for (let i = 0; i < entries.length; i++) {
704
- const [key, record] = entries[i];
705
- if (record.resetTime <= now) {
706
- hits.delete(key);
816
+ const fn = middleware[i];
817
+ if (!context[$debug]) {
818
+ return fn(context, () => runner(i + 1));
819
+ }
820
+ const debug = context[$debug];
821
+ const debugId = fn._debugId || fn.name || "anonymous";
822
+ const previousNode = debug.getCurrentNode();
823
+ debug.trackEdge(previousNode, debugId);
824
+ debug.setNode(debugId);
825
+ const start = performance.now();
826
+ try {
827
+ const res = await Promise.resolve(fn(context, () => runner(i + 1)));
828
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
829
+ return res;
830
+ } catch (err) {
831
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
832
+ return Promise.reject(err);
833
+ } finally {
834
+ if (previousNode) debug.setNode(previousNode);
707
835
  }
708
836
  }
709
- }, windowMs);
710
- if (interval.unref) interval.unref();
711
- const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
712
- if (skip(ctx)) return next();
713
- const key = keyGenerator(ctx);
714
- const now = Date.now();
715
- let record = hits.get(key);
716
- if (!record || record.resetTime <= now) {
717
- record = {
718
- hits: 0,
719
- resetTime: now + windowMs
720
- };
721
- hits.set(key, record);
722
- }
723
- record.hits++;
724
- const remaining = Math.max(0, max - record.hits);
725
- const resetTime = Math.ceil(record.resetTime / 1e3);
726
- const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
727
- const setHeaders = (res) => {
728
- if (!headers || !res || !res.headers) return;
729
- try {
730
- res.headers.set("X-RateLimit-Limit", String(max));
731
- res.headers.set("X-RateLimit-Remaining", String(remaining));
732
- res.headers.set("X-RateLimit-Reset", String(resetTime));
733
- } catch (e) {
734
- }
735
- };
736
- if (record.hits > max) {
737
- typeof message === "object" ? JSON.stringify(message) : String(message);
738
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
739
- if (headers) {
740
- setHeaders(res);
741
- res.headers.set("Retry-After", String(retryAfter));
742
- }
743
- return res;
744
- }
745
- const response = await next();
746
- if (response instanceof Response && headers) {
747
- setHeaders(response);
748
- }
749
- return response;
750
- };
751
- rateLimitMiddleware.isBuiltin = true;
752
- rateLimitMiddleware.pluginName = "RateLimit";
753
- return rateLimitMiddleware;
754
- }
755
- const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
756
- const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
757
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
758
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
759
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
760
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
761
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
762
- const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
763
- const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
764
- const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
765
- const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
766
- const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
767
- const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
768
- const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
769
- const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
770
- const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
771
- var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
772
- RouteParamType2["BODY"] = "BODY";
773
- RouteParamType2["PARAM"] = "PARAM";
774
- RouteParamType2["QUERY"] = "QUERY";
775
- RouteParamType2["HEADER"] = "HEADER";
776
- RouteParamType2["REQUEST"] = "REQUEST";
777
- RouteParamType2["CONTEXT"] = "CONTEXT";
778
- return RouteParamType2;
779
- })(RouteParamType || {});
780
- function Controller(path2 = "/") {
781
- return (target) => {
782
- target[$controllerPath] = path2;
783
- };
784
- }
785
- function Use(...middleware) {
786
- return (target, propertyKey, descriptor) => {
787
- if (!propertyKey) {
788
- const existing = target[$middleware] || [];
789
- target[$middleware] = [...existing, ...middleware];
790
- } else {
791
- if (!target[$middleware]) {
792
- target[$middleware] = /* @__PURE__ */ new Map();
793
- }
794
- const existing = target[$middleware].get(propertyKey) || [];
795
- target[$middleware].set(propertyKey, [...existing, ...middleware]);
796
- }
797
- };
798
- }
799
- function createParamDecorator(type) {
800
- return (name) => {
801
- return (target, propertyKey, parameterIndex) => {
802
- if (!target[$routeArgs]) {
803
- target[$routeArgs] = /* @__PURE__ */ new Map();
804
- }
805
- if (!target[$routeArgs].has(propertyKey)) {
806
- target[$routeArgs].set(propertyKey, []);
807
- }
808
- target[$routeArgs].get(propertyKey).push({
809
- index: parameterIndex,
810
- type,
811
- name
812
- });
813
- };
814
- };
815
- }
816
- const Body = createParamDecorator(RouteParamType.BODY);
817
- const Param = createParamDecorator(RouteParamType.PARAM);
818
- const Query = createParamDecorator(RouteParamType.QUERY);
819
- const Headers$1 = createParamDecorator(RouteParamType.HEADER);
820
- const Req = createParamDecorator(RouteParamType.REQUEST);
821
- const Ctx = createParamDecorator(RouteParamType.CONTEXT);
822
- function Spec(spec) {
823
- return (target, propertyKey, descriptor) => {
824
- if (!target[$routeSpec]) {
825
- target[$routeSpec] = /* @__PURE__ */ new Map();
826
- }
827
- target[$routeSpec].set(propertyKey, spec);
828
- };
829
- }
830
- function createMethodDecorator(method) {
831
- return (path2 = "/") => {
832
- return (target, propertyKey, descriptor) => {
833
- if (!target[$routeMethods]) {
834
- target[$routeMethods] = /* @__PURE__ */ new Map();
835
- }
836
- target[$routeMethods].set(propertyKey, {
837
- method,
838
- path: path2
839
- });
840
- };
841
- };
842
- }
843
- const Get = createMethodDecorator("GET");
844
- const Post = createMethodDecorator("POST");
845
- const Put = createMethodDecorator("PUT");
846
- const Delete = createMethodDecorator("DELETE");
847
- const Patch = createMethodDecorator("PATCH");
848
- const Options = createMethodDecorator("OPTIONS");
849
- const Head = createMethodDecorator("HEAD");
850
- const All = createMethodDecorator("ALL");
851
- function RateLimit(options) {
852
- return Use(RateLimitMiddleware(options));
853
- }
854
- class Container {
855
- static services = /* @__PURE__ */ new Map();
856
- static register(target, instance) {
857
- this.services.set(target, instance);
858
- }
859
- static get(target) {
860
- return this.services.get(target);
861
- }
862
- static has(target) {
863
- return this.services.has(target);
864
- }
865
- static resolve(target) {
866
- if (this.services.has(target)) {
867
- return this.services.get(target);
868
- }
869
- const instance = new target();
870
- this.services.set(target, instance);
871
- return instance;
872
- }
873
- }
874
- function Injectable() {
875
- return (target) => {
876
- };
877
- }
878
- function Inject(token) {
879
- return (target, key) => {
880
- Object.defineProperty(target, key, {
881
- get: () => Container.resolve(token),
882
- enumerable: true,
883
- configurable: true
884
- });
837
+ return runner(0);
885
838
  };
886
- }
887
- const compose = (middleware) => {
888
- if (!middleware.length) {
889
- return (context, next) => {
890
- return next ? next() : Promise.resolve();
891
- };
892
- }
893
- return function dispatch(context, next) {
894
- let index = -1;
895
- async function runner(i) {
896
- if (i <= index) return Promise.reject(new Error("next() called multiple times"));
897
- index = i;
898
- if (i >= middleware.length) {
899
- return next ? next() : Promise.resolve();
900
- }
901
- const fn = middleware[i];
902
- if (!context._debug) {
903
- return fn(context, () => runner(i + 1));
839
+ };
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"
904
848
  }
905
- const debug = context._debug;
906
- const debugId = fn._debugId || fn.name || "anonymous";
907
- const previousNode = debug.getCurrentNode();
908
- debug.trackEdge(previousNode, debugId);
909
- debug.setNode(debugId);
910
- const start = performance.now();
849
+ }, async (span) => {
911
850
  try {
912
- const res = await Promise.resolve(fn(context, () => runner(i + 1)));
913
- debug.trackStep(debugId, "middleware", performance.now() - start, "success");
914
- return res;
851
+ const result = await fn.apply(this, args);
852
+ return result;
915
853
  } catch (err) {
916
- debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
917
- return Promise.reject(err);
854
+ span.recordException(err);
855
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
856
+ throw err;
918
857
  } finally {
919
- if (previousNode) debug.setNode(previousNode);
858
+ span.end();
920
859
  }
921
- }
922
- return runner(0);
860
+ });
923
861
  };
924
- };
925
- class ShokupanRequestBase {
926
- method;
927
- url;
928
- headers;
929
- body;
930
- async json() {
931
- return JSON.parse(this.body);
932
- }
933
- async text() {
934
- return this.body;
935
- }
936
- async formData() {
937
- if (this.body instanceof FormData) {
938
- return this.body;
939
- }
940
- return new Response(this.body, { headers: this.headers }).formData();
941
- }
942
- constructor(props) {
943
- Object.assign(this, props);
944
- if (!(this.headers instanceof Headers)) {
945
- this.headers = new Headers(this.headers);
946
- }
947
- }
948
862
  }
949
- const ShokupanRequest = ShokupanRequestBase;
950
863
  function isObject(item) {
951
864
  return item && typeof item === "object" && !Array.isArray(item);
952
865
  }
@@ -1172,9 +1085,9 @@ async function generateOpenApi(rootRouter, options = {}) {
1172
1085
  const defaultTagName = options.defaultTag || "Application";
1173
1086
  let astRoutes = [];
1174
1087
  try {
1175
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-Bei1sVWp.cjs"));
1176
- const analyzer = new OpenAPIAnalyzer(process.cwd());
1177
- const { applications } = await analyzer.analyze();
1088
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-Bei1sVWp.cjs"));
1089
+ const analyzer2 = new OpenAPIAnalyzer(process.cwd());
1090
+ const { applications } = await analyzer2.analyze();
1178
1091
  astRoutes = await getAstRoutes(applications);
1179
1092
  } catch (e) {
1180
1093
  }
@@ -1361,6 +1274,40 @@ async function generateOpenApi(rootRouter, options = {}) {
1361
1274
  "x-tagGroups": xTagGroups
1362
1275
  };
1363
1276
  }
1277
+ class RequestContextStore {
1278
+ request;
1279
+ span;
1280
+ }
1281
+ const asyncContext = new node_async_hooks.AsyncLocalStorage();
1282
+ class HttpError extends Error {
1283
+ status;
1284
+ constructor(message, status) {
1285
+ super(message);
1286
+ this.name = "HttpError";
1287
+ this.status = status;
1288
+ if (Error.captureStackTrace) {
1289
+ Error.captureStackTrace(this, HttpError);
1290
+ }
1291
+ }
1292
+ }
1293
+ function getErrorStatus(err) {
1294
+ if (!err || typeof err !== "object") {
1295
+ return 500;
1296
+ }
1297
+ if (typeof err.status === "number") {
1298
+ return err.status;
1299
+ }
1300
+ if (typeof err.statusCode === "number") {
1301
+ return err.statusCode;
1302
+ }
1303
+ return 500;
1304
+ }
1305
+ class EventError extends HttpError {
1306
+ constructor(message = "Event Error") {
1307
+ super(message, 500);
1308
+ this.name = "EventError";
1309
+ }
1310
+ }
1364
1311
  const eta$1 = new eta$2.Eta();
1365
1312
  function serveStatic(config, prefix) {
1366
1313
  const rootPath = path.resolve(config.root || ".");
@@ -1513,14 +1460,162 @@ function serveStatic(config, prefix) {
1513
1460
  serveStaticMiddleware.pluginName = "ServeStatic";
1514
1461
  return serveStaticMiddleware;
1515
1462
  }
1516
- class RouterTrie {
1517
- root;
1518
- constructor() {
1519
- this.root = this.createNode();
1520
- }
1521
- createNode() {
1522
- return {
1523
- children: {}
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
+ class Container {
1525
+ static services = /* @__PURE__ */ new Map();
1526
+ static register(target, instance) {
1527
+ this.services.set(target, instance);
1528
+ }
1529
+ static get(target) {
1530
+ return this.services.get(target);
1531
+ }
1532
+ static has(target) {
1533
+ return this.services.has(target);
1534
+ }
1535
+ static resolve(target) {
1536
+ if (this.services.has(target)) {
1537
+ return this.services.get(target);
1538
+ }
1539
+ const instance = new target();
1540
+ this.services.set(target, instance);
1541
+ return instance;
1542
+ }
1543
+ }
1544
+ function Injectable() {
1545
+ return (target) => {
1546
+ };
1547
+ }
1548
+ function Inject(token) {
1549
+ return (target, key) => {
1550
+ Object.defineProperty(target, key, {
1551
+ get: () => Container.resolve(token),
1552
+ enumerable: true,
1553
+ configurable: true
1554
+ });
1555
+ };
1556
+ }
1557
+ class ShokupanRequestBase {
1558
+ method;
1559
+ url;
1560
+ headers;
1561
+ body;
1562
+ async json() {
1563
+ return JSON.parse(this.body);
1564
+ }
1565
+ async text() {
1566
+ return this.body;
1567
+ }
1568
+ async formData() {
1569
+ if (this.body instanceof FormData) {
1570
+ return this.body;
1571
+ }
1572
+ return new Response(this.body, { headers: this.headers }).formData();
1573
+ }
1574
+ constructor(props) {
1575
+ Object.assign(this, props);
1576
+ if (!(this.headers instanceof Headers)) {
1577
+ this.headers = new Headers(this.headers);
1578
+ }
1579
+ }
1580
+ }
1581
+ const ShokupanRequest = ShokupanRequestBase;
1582
+ function getCallerInfo(skipFrames = 1) {
1583
+ let file = "unknown";
1584
+ let line = 0;
1585
+ try {
1586
+ const err = new Error();
1587
+ const stack = err.stack?.split("\n") || [];
1588
+ let found = 0;
1589
+ for (let i = 1; i < stack.length; i++) {
1590
+ const l = stack[i];
1591
+ if (!l.includes(":")) continue;
1592
+ if (l.includes("node_modules")) continue;
1593
+ if (l.includes("bun:main")) continue;
1594
+ if (l.includes("src/util/stack.ts")) continue;
1595
+ if (l.includes("src/router.ts")) continue;
1596
+ if (l.includes("src/shokupan.ts")) continue;
1597
+ found++;
1598
+ if (found >= skipFrames) {
1599
+ const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1600
+ if (match) {
1601
+ file = match[1];
1602
+ line = parseInt(match[2], 10);
1603
+ return { file, line };
1604
+ }
1605
+ }
1606
+ }
1607
+ } catch (e) {
1608
+ }
1609
+ return { file, line };
1610
+ }
1611
+ class RouterTrie {
1612
+ root;
1613
+ constructor() {
1614
+ this.root = this.createNode();
1615
+ }
1616
+ createNode() {
1617
+ return {
1618
+ children: {}
1524
1619
  };
1525
1620
  }
1526
1621
  insert(method, path2, handler) {
@@ -1613,124 +1708,16 @@ class RouterTrie {
1613
1708
  return s.split("/");
1614
1709
  }
1615
1710
  }
1616
- const asyncContext = new node_async_hooks.AsyncLocalStorage();
1617
- let db;
1618
- let dbPromise = null;
1619
- let RecordId;
1620
- async function ensureDb() {
1621
- if (db) return db;
1622
- if (dbPromise) return dbPromise;
1623
- dbPromise = (async () => {
1624
- try {
1625
- const { createNodeEngines } = await import("@surrealdb/node");
1626
- const surreal = await import("surrealdb");
1627
- const Surreal = surreal.Surreal;
1628
- RecordId = surreal.RecordId;
1629
- const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1630
- const _db = new Surreal({
1631
- engines: createNodeEngines()
1632
- });
1633
- await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1634
- await _db.query(`
1635
- DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1636
- DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1637
- DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1638
- DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1639
- DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1640
- DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1641
- `);
1642
- db = _db;
1643
- return db;
1644
- } catch (e) {
1645
- dbPromise = null;
1646
- if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1647
- throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1648
- }
1649
- throw e;
1650
- }
1651
- })();
1652
- return dbPromise;
1653
- }
1654
- const datastore = {
1655
- async get(store, key) {
1656
- await ensureDb();
1657
- return db.select(new RecordId(store, key));
1658
- },
1659
- async set(store, key, value) {
1660
- await ensureDb();
1661
- return db.create(new RecordId(store, key)).content(value);
1662
- },
1663
- async query(query, vars) {
1664
- await ensureDb();
1665
- try {
1666
- const r = await db.query(query, vars);
1667
- return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
1668
- } catch (e) {
1669
- console.error("DS ERROR:", e);
1670
- throw e;
1671
- }
1672
- },
1673
- get ready() {
1674
- return ensureDb().then(() => void 0);
1675
- }
1676
- };
1677
- process.on("exit", async () => {
1678
- if (db) await db.close();
1679
- });
1680
- const tracer = api.trace.getTracer("shokupan.middleware");
1681
- function traceHandler(fn, name) {
1682
- return async function(...args) {
1683
- return tracer.startActiveSpan(`route handler - ${name}`, {
1684
- kind: api.SpanKind.INTERNAL,
1685
- attributes: {
1686
- "http.route": name,
1687
- "component": "shokupan.route"
1688
- }
1689
- }, async (span) => {
1690
- try {
1691
- const result = await fn.apply(this, args);
1692
- return result;
1693
- } catch (err) {
1694
- span.recordException(err);
1695
- span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
1696
- throw err;
1697
- } finally {
1698
- span.end();
1699
- }
1700
- });
1701
- };
1702
- }
1703
- function getCallerInfo(skipFrames = 1) {
1704
- let file = "unknown";
1705
- let line = 0;
1706
- try {
1707
- const err = new Error();
1708
- const stack = err.stack?.split("\n") || [];
1709
- let found = 0;
1710
- for (let i = 1; i < stack.length; i++) {
1711
- const l = stack[i];
1712
- if (!l.includes(":")) continue;
1713
- if (l.includes("node_modules")) continue;
1714
- if (l.includes("bun:main")) continue;
1715
- if (l.includes("src/util/stack.ts")) continue;
1716
- if (l.includes("src/router.ts")) continue;
1717
- if (l.includes("src/shokupan.ts")) continue;
1718
- found++;
1719
- if (found >= skipFrames) {
1720
- const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1721
- if (match) {
1722
- file = match[1];
1723
- line = parseInt(match[2], 10);
1724
- return { file, line };
1725
- }
1726
- }
1727
- }
1728
- } catch (e) {
1729
- }
1730
- return { file, line };
1731
- }
1732
- const RouterRegistry = /* @__PURE__ */ new Map();
1733
- const ShokupanApplicationTree = {};
1711
+ const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1712
+ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1713
+ RouteParamType2["BODY"] = "BODY";
1714
+ RouteParamType2["PARAM"] = "PARAM";
1715
+ RouteParamType2["QUERY"] = "QUERY";
1716
+ RouteParamType2["HEADER"] = "HEADER";
1717
+ RouteParamType2["REQUEST"] = "REQUEST";
1718
+ RouteParamType2["CONTEXT"] = "CONTEXT";
1719
+ return RouteParamType2;
1720
+ })(RouteParamType || {});
1734
1721
  class ShokupanRouter {
1735
1722
  constructor(config) {
1736
1723
  this.config = config;
@@ -1763,6 +1750,7 @@ class ShokupanRouter {
1763
1750
  metadata;
1764
1751
  // Metadata for the router itself
1765
1752
  currentGuards = [];
1753
+ eventHandlers = /* @__PURE__ */ new Map();
1766
1754
  // Registry Accessor
1767
1755
  getComponentRegistry() {
1768
1756
  const controllerRoutesMap = /* @__PURE__ */ new Map();
@@ -1823,6 +1811,34 @@ class ShokupanRouter {
1823
1811
  isRouterInstance(target) {
1824
1812
  return typeof target === "object" && target !== null && $isRouter in target;
1825
1813
  }
1814
+ /**
1815
+ * Registers an event handler for WebSocket.
1816
+ */
1817
+ event(name, handler) {
1818
+ if (this.eventHandlers.has(name)) {
1819
+ const err = new EventError(`Event handler \`${name}\` already exists.`);
1820
+ console.warn(err);
1821
+ const handlers = this.eventHandlers.get(name);
1822
+ handlers.push(handler);
1823
+ this.eventHandlers.set(name, handlers);
1824
+ } else {
1825
+ this.eventHandlers.set(name, [handler]);
1826
+ }
1827
+ return this;
1828
+ }
1829
+ /**
1830
+ * Finds an event handler(s) by name.
1831
+ */
1832
+ findEvent(name) {
1833
+ if (this.eventHandlers.has(name)) {
1834
+ return this.eventHandlers.get(name);
1835
+ }
1836
+ for (const child of this[$childRouters]) {
1837
+ const handler = child.findEvent(name);
1838
+ if (handler) return handler;
1839
+ }
1840
+ return null;
1841
+ }
1826
1842
  /**
1827
1843
  * Mounts a controller instance to a path prefix.
1828
1844
  *
@@ -1841,216 +1857,9 @@ class ShokupanRouter {
1841
1857
  throw new Error(`[Shokupan] strict controller check failed: ${controller.constructor.name || typeof controller} is not a class constructor.`);
1842
1858
  }
1843
1859
  if (this.isRouterInstance(controller)) {
1844
- if (controller[$isMounted]) {
1845
- throw new Error("Router is already mounted");
1846
- }
1847
- controller[$mountPath] = prefix;
1848
- if (!controller.metadata) {
1849
- const info = getCallerInfo();
1850
- controller.metadata = {
1851
- file: info.file,
1852
- line: info.line,
1853
- name: "MountedRouter"
1854
- };
1855
- }
1856
- this[$childRouters].push(controller);
1857
- controller[$parent] = this;
1858
- const setRouterContext = (router) => {
1859
- router[$appRoot] = this.root;
1860
- router[$childRouters].forEach((child) => setRouterContext(child));
1861
- };
1862
- setRouterContext(controller);
1863
- if (this[$appRoot]) ;
1864
- controller[$appRoot] = this.root;
1865
- controller[$isMounted] = true;
1860
+ this.mountRouter(prefix, controller);
1866
1861
  } else {
1867
- let instance = controller;
1868
- if (typeof controller === "function") {
1869
- instance = Container.resolve(controller);
1870
- const controllerPath = controller[$controllerPath];
1871
- if (controllerPath) {
1872
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1873
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1874
- prefix = p1 + p2;
1875
- if (!prefix) prefix = "/";
1876
- }
1877
- } else {
1878
- const ctor = instance.constructor;
1879
- const controllerPath = ctor[$controllerPath];
1880
- if (controllerPath) {
1881
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1882
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1883
- prefix = p1 + p2;
1884
- if (!prefix) prefix = "/";
1885
- }
1886
- }
1887
- instance[$mountPath] = prefix;
1888
- const info = getCallerInfo();
1889
- instance.metadata = {
1890
- file: info.file,
1891
- line: info.line,
1892
- name: instance.constructor.name
1893
- };
1894
- this[$childControllers].push(instance);
1895
- const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1896
- const proto = Object.getPrototypeOf(instance);
1897
- const methods = /* @__PURE__ */ new Set();
1898
- let current = proto;
1899
- while (current && current !== Object.prototype) {
1900
- Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1901
- current = Object.getPrototypeOf(current);
1902
- }
1903
- Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1904
- const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1905
- const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1906
- const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1907
- let routesAttached = 0;
1908
- for (let i = 0; i < Array.from(methods).length; i++) {
1909
- const name = Array.from(methods)[i];
1910
- if (name === "constructor") continue;
1911
- if (["arguments", "caller", "callee"].includes(name)) continue;
1912
- const originalHandler = instance[name];
1913
- if (typeof originalHandler !== "function") continue;
1914
- let method;
1915
- let subPath = "";
1916
- if (decoratedRoutes && decoratedRoutes.has(name)) {
1917
- const config = decoratedRoutes.get(name);
1918
- method = config.method;
1919
- subPath = config.path;
1920
- } else {
1921
- for (let j = 0; j < HTTPMethods.length; j++) {
1922
- const m = HTTPMethods[j];
1923
- if (name.toUpperCase().startsWith(m)) {
1924
- method = m;
1925
- const rest = name.slice(m.length);
1926
- if (rest.length === 0) {
1927
- subPath = "/";
1928
- } else {
1929
- subPath = "";
1930
- let buffer = "";
1931
- const flush = () => {
1932
- if (buffer.length > 0) {
1933
- subPath += "/" + buffer.toLowerCase();
1934
- buffer = "";
1935
- }
1936
- };
1937
- for (let i2 = 0; i2 < rest.length; i2++) {
1938
- const char = rest[i2];
1939
- if (char === "$") {
1940
- flush();
1941
- subPath += "/:";
1942
- continue;
1943
- }
1944
- }
1945
- subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1946
- if (!subPath.startsWith("/")) {
1947
- subPath = "/" + subPath;
1948
- }
1949
- }
1950
- break;
1951
- }
1952
- }
1953
- }
1954
- if (method) {
1955
- routesAttached++;
1956
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1957
- const cleanSubPath = subPath === "/" ? "" : subPath;
1958
- let joined;
1959
- if (cleanSubPath.length === 0) {
1960
- joined = cleanPrefix;
1961
- } else if (cleanSubPath.startsWith("/")) {
1962
- joined = cleanPrefix + cleanSubPath;
1963
- } else {
1964
- joined = cleanPrefix + "/" + cleanSubPath;
1965
- }
1966
- const fullPath = joined || "/";
1967
- const normalizedPath = fullPath.replace(/\/+/g, "/");
1968
- const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1969
- const allMiddleware = [...controllerMiddleware, ...methodMw];
1970
- const routeArgs = decoratedArgs && decoratedArgs.get(name);
1971
- const wrappedHandler = async (ctx) => {
1972
- let args = [ctx];
1973
- if (routeArgs?.length > 0) {
1974
- args = [];
1975
- const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1976
- for (let k = 0; k < sortedArgs.length; k++) {
1977
- const arg = sortedArgs[k];
1978
- switch (arg.type) {
1979
- case RouteParamType.BODY:
1980
- try {
1981
- if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1982
- args[arg.index] = await ctx.req.json();
1983
- } else {
1984
- const text = await ctx.req.text();
1985
- if (!text) {
1986
- args[arg.index] = {};
1987
- } else {
1988
- args[arg.index] = JSON.parse(text);
1989
- }
1990
- }
1991
- } catch (e) {
1992
- const err = new Error("Invalid JSON body");
1993
- err.status = 400;
1994
- throw err;
1995
- }
1996
- break;
1997
- case RouteParamType.PARAM:
1998
- args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1999
- break;
2000
- case RouteParamType.QUERY: {
2001
- const url = new URL(ctx.req.url);
2002
- if (arg.name) {
2003
- const vals = url.searchParams.getAll(arg.name);
2004
- args[arg.index] = vals.length > 1 ? vals : vals[0];
2005
- } else {
2006
- const query = {};
2007
- const keys = Object.keys(url.searchParams);
2008
- for (let k2 = 0; k2 < keys.length; k2++) {
2009
- const key = keys[k2];
2010
- const vals = url.searchParams.getAll(key);
2011
- query[key] = vals.length > 1 ? vals : vals[0];
2012
- }
2013
- args[arg.index] = query;
2014
- }
2015
- break;
2016
- }
2017
- case RouteParamType.HEADER:
2018
- args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2019
- break;
2020
- case RouteParamType.REQUEST:
2021
- args[arg.index] = ctx.req;
2022
- break;
2023
- case RouteParamType.CONTEXT:
2024
- args[arg.index] = ctx;
2025
- break;
2026
- }
2027
- }
2028
- }
2029
- const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
2030
- return tracedOriginalHandler.apply(instance, args);
2031
- };
2032
- let finalHandler = wrappedHandler;
2033
- if (allMiddleware.length > 0) {
2034
- const composed = compose(allMiddleware);
2035
- finalHandler = async (ctx) => {
2036
- return composed(ctx, () => wrappedHandler(ctx));
2037
- };
2038
- }
2039
- finalHandler.originalHandler = originalHandler;
2040
- if (finalHandler !== wrappedHandler) {
2041
- wrappedHandler.originalHandler = originalHandler;
2042
- }
2043
- const tagName = instance.constructor.name;
2044
- const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2045
- const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2046
- const spec = { tags: [tagName], ...userSpec };
2047
- this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2048
- }
2049
- }
2050
- if (routesAttached === 0) {
2051
- console.warn(`No routes attached to controller ${instance.constructor.name}`);
2052
- }
2053
- instance[$isMounted] = true;
1862
+ this.scanControllerRoutes(prefix, controller);
2054
1863
  }
2055
1864
  return this;
2056
1865
  }
@@ -2088,8 +1897,6 @@ class ShokupanRouter {
2088
1897
  */
2089
1898
  async internalRequest(arg) {
2090
1899
  const options = typeof arg === "string" ? { path: arg } : arg;
2091
- const store = asyncContext.getStore();
2092
- store?.get("req");
2093
1900
  let url = options.path;
2094
1901
  if (!url.startsWith("http")) {
2095
1902
  const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig.port || 3e3}`;
@@ -2132,7 +1939,7 @@ class ShokupanRouter {
2132
1939
  });
2133
1940
  const ctx = new ShokupanContext(req);
2134
1941
  let result = null;
2135
- let status = 200;
1942
+ let status = HTTP_STATUS.OK;
2136
1943
  const headers = {};
2137
1944
  const match = this.find(req.method, ctx.path);
2138
1945
  if (match) {
@@ -2141,12 +1948,12 @@ class ShokupanRouter {
2141
1948
  result = await match.handler(ctx);
2142
1949
  } catch (err) {
2143
1950
  console.error(err);
2144
- status = err.status || err.statusCode || 500;
1951
+ status = getErrorStatus(err);
2145
1952
  result = { error: err.message || "Internal Server Error" };
2146
1953
  if (err.errors) result.errors = err.errors;
2147
1954
  }
2148
1955
  } else {
2149
- status = 404;
1956
+ status = HTTP_STATUS.NOT_FOUND;
2150
1957
  result = "Not Found";
2151
1958
  }
2152
1959
  if (result instanceof Response) {
@@ -2175,7 +1982,7 @@ class ShokupanRouter {
2175
1982
  const originalHandler = handler;
2176
1983
  const wrapped = async (ctx) => {
2177
1984
  await this.runHooks("onRequestStart", ctx);
2178
- const debug = ctx._debug;
1985
+ const debug = ctx[$debug];
2179
1986
  let debugId;
2180
1987
  let previousNode;
2181
1988
  if (debug) {
@@ -2201,6 +2008,254 @@ class ShokupanRouter {
2201
2008
  wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
2202
2009
  return wrapped;
2203
2010
  }
2011
+ mountRouter(prefix, router) {
2012
+ if (router[$isMounted]) {
2013
+ throw new Error("Router is already mounted");
2014
+ }
2015
+ router[$mountPath] = prefix;
2016
+ if (!router.metadata) {
2017
+ const info = getCallerInfo();
2018
+ router.metadata = {
2019
+ file: info.file,
2020
+ line: info.line,
2021
+ name: "MountedRouter"
2022
+ };
2023
+ }
2024
+ this[$childRouters].push(router);
2025
+ router[$parent] = this;
2026
+ const setRouterContext = (router2) => {
2027
+ router2[$appRoot] = this.root;
2028
+ router2[$childRouters].forEach((child) => setRouterContext(child));
2029
+ };
2030
+ setRouterContext(router);
2031
+ router[$appRoot] = this.root;
2032
+ router[$isMounted] = true;
2033
+ }
2034
+ scanControllerRoutes(prefix, controller) {
2035
+ let instance = controller;
2036
+ if (typeof controller === "function") {
2037
+ instance = Container.resolve(controller);
2038
+ const controllerPath = controller[$controllerPath];
2039
+ if (controllerPath) {
2040
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2041
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
2042
+ prefix = p1 + p2;
2043
+ if (!prefix) prefix = "/";
2044
+ }
2045
+ } else {
2046
+ const ctor = instance.constructor;
2047
+ const controllerPath = ctor[$controllerPath];
2048
+ if (controllerPath) {
2049
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2050
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
2051
+ prefix = p1 + p2;
2052
+ if (!prefix) prefix = "/";
2053
+ }
2054
+ }
2055
+ instance[$mountPath] = prefix;
2056
+ const info = getCallerInfo();
2057
+ instance.metadata = {
2058
+ file: info.file,
2059
+ line: info.line,
2060
+ name: instance.constructor.name
2061
+ };
2062
+ this[$childControllers].push(instance);
2063
+ const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
2064
+ const proto = Object.getPrototypeOf(instance);
2065
+ const methods = /* @__PURE__ */ new Set();
2066
+ let current = proto;
2067
+ while (current && current !== Object.prototype) {
2068
+ Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
2069
+ current = Object.getPrototypeOf(current);
2070
+ }
2071
+ Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
2072
+ const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
2073
+ const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
2074
+ const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
2075
+ const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
2076
+ let routesAttached = 0;
2077
+ for (let i = 0; i < Array.from(methods).length; i++) {
2078
+ const name = Array.from(methods)[i];
2079
+ if (name === "constructor") continue;
2080
+ if (["arguments", "caller", "callee"].includes(name)) continue;
2081
+ const originalHandler = instance[name];
2082
+ if (typeof originalHandler !== "function") continue;
2083
+ let method;
2084
+ let subPath = "";
2085
+ if (decoratedRoutes && decoratedRoutes.has(name)) {
2086
+ const config = decoratedRoutes.get(name);
2087
+ method = config.method;
2088
+ subPath = config.path;
2089
+ } else {
2090
+ for (let j = 0; j < HTTPMethods.length; j++) {
2091
+ const m = HTTPMethods[j];
2092
+ if (name.toUpperCase().startsWith(m)) {
2093
+ method = m;
2094
+ const rest = name.slice(m.length);
2095
+ if (rest.length === 0) {
2096
+ subPath = "/";
2097
+ } else {
2098
+ subPath = "";
2099
+ let buffer = "";
2100
+ const flush = () => {
2101
+ if (buffer.length > 0) {
2102
+ subPath += "/" + buffer.toLowerCase();
2103
+ buffer = "";
2104
+ }
2105
+ };
2106
+ for (let i2 = 0; i2 < rest.length; i2++) {
2107
+ const char = rest[i2];
2108
+ if (char === "$") {
2109
+ flush();
2110
+ subPath += "/:";
2111
+ continue;
2112
+ }
2113
+ buffer += char;
2114
+ }
2115
+ if (buffer.length > 0) flush();
2116
+ subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
2117
+ if (!subPath.startsWith("/")) {
2118
+ subPath = "/" + subPath;
2119
+ }
2120
+ }
2121
+ break;
2122
+ }
2123
+ }
2124
+ }
2125
+ if (method) {
2126
+ routesAttached++;
2127
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2128
+ const cleanSubPath = subPath === "/" ? "" : subPath;
2129
+ let joined;
2130
+ if (cleanSubPath.length === 0) {
2131
+ joined = cleanPrefix;
2132
+ } else if (cleanSubPath.startsWith("/")) {
2133
+ joined = cleanPrefix + cleanSubPath;
2134
+ } else {
2135
+ joined = cleanPrefix + "/" + cleanSubPath;
2136
+ }
2137
+ const fullPath = joined || "/";
2138
+ const normalizedPath = fullPath.replace(/\/+/g, "/");
2139
+ const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
2140
+ const allMiddleware = [...controllerMiddleware, ...methodMw];
2141
+ const routeArgs = decoratedArgs && decoratedArgs.get(name);
2142
+ const wrappedHandler = async (ctx) => {
2143
+ let args = [ctx];
2144
+ if (routeArgs?.length > 0) {
2145
+ args = [];
2146
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2147
+ for (let k = 0; k < sortedArgs.length; k++) {
2148
+ const arg = sortedArgs[k];
2149
+ switch (arg.type) {
2150
+ case RouteParamType.BODY:
2151
+ try {
2152
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
2153
+ args[arg.index] = await ctx.req.json();
2154
+ } else {
2155
+ const text = await ctx.req.text();
2156
+ if (!text) {
2157
+ args[arg.index] = {};
2158
+ } else {
2159
+ args[arg.index] = JSON.parse(text);
2160
+ }
2161
+ }
2162
+ } catch (e) {
2163
+ const err = new Error("Invalid JSON body");
2164
+ err.status = 400;
2165
+ throw err;
2166
+ }
2167
+ break;
2168
+ case RouteParamType.PARAM:
2169
+ args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
2170
+ break;
2171
+ case RouteParamType.QUERY: {
2172
+ const url = new URL(ctx.req.url);
2173
+ if (arg.name) {
2174
+ const vals = url.searchParams.getAll(arg.name);
2175
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
2176
+ } else {
2177
+ const query = {};
2178
+ const keys = Object.keys(url.searchParams);
2179
+ for (let k2 = 0; k2 < keys.length; k2++) {
2180
+ const key = keys[k2];
2181
+ const vals = url.searchParams.getAll(key);
2182
+ query[key] = vals.length > 1 ? vals : vals[0];
2183
+ }
2184
+ args[arg.index] = query;
2185
+ }
2186
+ break;
2187
+ }
2188
+ case RouteParamType.HEADER:
2189
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2190
+ break;
2191
+ case RouteParamType.REQUEST:
2192
+ args[arg.index] = ctx.req;
2193
+ break;
2194
+ case RouteParamType.CONTEXT:
2195
+ args[arg.index] = ctx;
2196
+ break;
2197
+ }
2198
+ }
2199
+ }
2200
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
2201
+ return tracedOriginalHandler.apply(instance, args);
2202
+ };
2203
+ let finalHandler = wrappedHandler;
2204
+ if (allMiddleware.length > 0) {
2205
+ const composed = compose(allMiddleware);
2206
+ finalHandler = async (ctx) => {
2207
+ return composed(ctx, () => wrappedHandler(ctx));
2208
+ };
2209
+ }
2210
+ finalHandler.originalHandler = originalHandler;
2211
+ if (finalHandler !== wrappedHandler) {
2212
+ wrappedHandler.originalHandler = originalHandler;
2213
+ }
2214
+ const tagName = instance.constructor.name;
2215
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2216
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2217
+ const spec = { tags: [tagName], ...userSpec };
2218
+ this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2219
+ }
2220
+ if (decoratedEvents?.has(name)) {
2221
+ routesAttached++;
2222
+ const config = decoratedEvents.get(name);
2223
+ const routeArgs = decoratedArgs?.get(name);
2224
+ const wrappedHandler = async (ctx) => {
2225
+ let args = [ctx];
2226
+ if (routeArgs?.length > 0) {
2227
+ args = [];
2228
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2229
+ for (let k = 0; k < sortedArgs.length; k++) {
2230
+ const arg = sortedArgs[k];
2231
+ switch (arg.type) {
2232
+ case RouteParamType.BODY:
2233
+ args[arg.index] = await ctx.body();
2234
+ break;
2235
+ case RouteParamType.CONTEXT:
2236
+ args[arg.index] = ctx;
2237
+ break;
2238
+ case RouteParamType.REQUEST:
2239
+ args[arg.index] = ctx.req;
2240
+ break;
2241
+ case RouteParamType.HEADER:
2242
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2243
+ break;
2244
+ default:
2245
+ args[arg.index] = void 0;
2246
+ }
2247
+ }
2248
+ }
2249
+ return originalHandler.apply(instance, args);
2250
+ };
2251
+ this.event(config.eventName, wrappedHandler);
2252
+ }
2253
+ }
2254
+ if (routesAttached === 0) {
2255
+ console.warn(`No routes attached to controller ${instance.constructor.name}`);
2256
+ }
2257
+ instance[$isMounted] = true;
2258
+ }
2204
2259
  /**
2205
2260
  * Find a route matching the given method and path.
2206
2261
  * @param method HTTP method
@@ -2327,7 +2382,7 @@ class ShokupanRouter {
2327
2382
  if (effectiveRenderer) {
2328
2383
  const innerHandler = wrappedHandler;
2329
2384
  wrappedHandler = async (ctx) => {
2330
- ctx.renderer = effectiveRenderer;
2385
+ ctx.setRenderer(effectiveRenderer);
2331
2386
  return innerHandler(ctx);
2332
2387
  };
2333
2388
  }
@@ -2358,8 +2413,10 @@ class ShokupanRouter {
2358
2413
  Promise.resolve().then(async () => {
2359
2414
  try {
2360
2415
  const timestamp = Date.now();
2361
- const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2362
- await datastore.set("middleware_tracking", key, {
2416
+ await datastore.set(new surrealdb.RecordId("middleware_tracking", {
2417
+ timestamp,
2418
+ name: handler.name || "anonymous"
2419
+ }), {
2363
2420
  name: handler.name || "anonymous",
2364
2421
  path: ctx.path,
2365
2422
  timestamp,
@@ -2377,7 +2434,7 @@ class ShokupanRouter {
2377
2434
  const cutoff = Date.now() - ttl;
2378
2435
  await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2379
2436
  const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2380
- if (results && results[0] && results[0].count > maxCapacity) {
2437
+ if (results?.[0]?.count > maxCapacity) {
2381
2438
  const toDelete = results[0].count - maxCapacity;
2382
2439
  await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2383
2440
  }
@@ -2454,7 +2511,7 @@ class ShokupanRouter {
2454
2511
  (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
2455
2512
  );
2456
2513
  if (callerLine) {
2457
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
2514
+ const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
2458
2515
  if (match) {
2459
2516
  file = match[1];
2460
2517
  line = parseInt(match[2], 10);
@@ -2472,7 +2529,7 @@ class ShokupanRouter {
2472
2529
  }
2473
2530
  return guardHandler(ctx, next);
2474
2531
  };
2475
- trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
2532
+ trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
2476
2533
  this.currentGuards.push({ handler: trackedGuard, spec });
2477
2534
  return this;
2478
2535
  }
@@ -2585,7 +2642,7 @@ class ShokupanRouter {
2585
2642
  const fns = this.hookCache.get(name);
2586
2643
  if (!fns) return;
2587
2644
  const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2588
- const debug = ctx?._debug;
2645
+ const debug = ctx?.[$debug];
2589
2646
  if (debug) {
2590
2647
  await Promise.all(fns.map(async (fn, index) => {
2591
2648
  const hookId = `hook_${name}_${fn.name || index}`;
@@ -2658,6 +2715,7 @@ const defaults = {
2658
2715
  hostname: "localhost",
2659
2716
  development: process.env.NODE_ENV !== "production",
2660
2717
  enableAsyncLocalStorage: false,
2718
+ enableHttpBridge: false,
2661
2719
  reusePort: false
2662
2720
  };
2663
2721
  api.trace.getTracer("shokupan.application");
@@ -2666,6 +2724,7 @@ class Shokupan extends ShokupanRouter {
2666
2724
  openApiSpec;
2667
2725
  composedMiddleware;
2668
2726
  cpuMonitor;
2727
+ server;
2669
2728
  get logger() {
2670
2729
  return this.applicationConfig.logger;
2671
2730
  }
@@ -2729,6 +2788,13 @@ class Shokupan extends ShokupanRouter {
2729
2788
  }
2730
2789
  return this;
2731
2790
  }
2791
+ /**
2792
+ * Registers a plugin.
2793
+ */
2794
+ register(plugin, options) {
2795
+ plugin.onInit(this, options);
2796
+ return this;
2797
+ }
2732
2798
  startupHooks = [];
2733
2799
  /**
2734
2800
  * Registers a callback to be executed before the server starts listening.
@@ -2767,6 +2833,7 @@ class Shokupan extends ShokupanRouter {
2767
2833
  this.cpuMonitor = new SystemCpuMonitor();
2768
2834
  this.cpuMonitor.start();
2769
2835
  }
2836
+ const self = this;
2770
2837
  const serveOptions = {
2771
2838
  port: finalPort,
2772
2839
  hostname: this.applicationConfig.hostname,
@@ -2778,8 +2845,61 @@ class Shokupan extends ShokupanRouter {
2778
2845
  open(ws) {
2779
2846
  ws.data?.handler?.open?.(ws);
2780
2847
  },
2781
- message(ws, message) {
2782
- ws.data?.handler?.message?.(ws, message);
2848
+ async message(ws, message) {
2849
+ if (ws.data?.handler?.message) {
2850
+ return ws.data.handler.message(ws, message);
2851
+ }
2852
+ if (typeof message !== "string") return;
2853
+ try {
2854
+ const payload = JSON.parse(message);
2855
+ if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
2856
+ const { id, method, path: path2, headers, body } = payload;
2857
+ const url = new URL(path2, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
2858
+ const req = new Request(url.toString(), {
2859
+ method,
2860
+ headers,
2861
+ body: typeof body === "object" ? JSON.stringify(body) : body
2862
+ });
2863
+ const res = await self.fetch(req);
2864
+ const resBody = await res.json().catch((err) => res.text());
2865
+ const resHeaders = {};
2866
+ res.headers.forEach((v, k) => resHeaders[k] = v);
2867
+ ws.send(JSON.stringify({
2868
+ type: "RESPONSE",
2869
+ id,
2870
+ status: res.status,
2871
+ headers: resHeaders,
2872
+ body: resBody
2873
+ }));
2874
+ return;
2875
+ }
2876
+ const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
2877
+ if (eventName) {
2878
+ const handlers = self.findEvent(eventName);
2879
+ const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
2880
+ if (handler) {
2881
+ const data = payload.data || payload.payload;
2882
+ const req = new ShokupanRequest({
2883
+ url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
2884
+ method: "POST",
2885
+ headers: new Headers({ "content-type": "application/json" }),
2886
+ body: JSON.stringify(data)
2887
+ });
2888
+ const ctx = new ShokupanContext(req, self.server);
2889
+ ctx[$ws] = ws;
2890
+ try {
2891
+ await handler(ctx);
2892
+ } catch (err) {
2893
+ if (self.applicationConfig["websocketErrorHandler"]) {
2894
+ await self.applicationConfig["websocketErrorHandler"](err, ctx);
2895
+ } else {
2896
+ console.error(`Error in event ${eventName}:`, err);
2897
+ }
2898
+ }
2899
+ }
2900
+ }
2901
+ } catch (e) {
2902
+ }
2783
2903
  },
2784
2904
  drain(ws) {
2785
2905
  ws.data?.handler?.drain?.(ws);
@@ -2791,12 +2911,40 @@ class Shokupan extends ShokupanRouter {
2791
2911
  };
2792
2912
  let factory = this.applicationConfig.serverFactory;
2793
2913
  if (!factory && typeof Bun === "undefined") {
2794
- const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-DFhwlK8e.cjs"));
2914
+ const { createHttpServer } = await Promise.resolve().then(() => require("./http-server-BEMPIs33.cjs"));
2795
2915
  factory = createHttpServer();
2796
2916
  }
2797
- const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2917
+ this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2798
2918
  console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
2799
- return server;
2919
+ return this.server;
2920
+ }
2921
+ /**
2922
+ * Stops the application server.
2923
+ *
2924
+ * This method gracefully shuts down the server and stops any running monitors.
2925
+ * Works transparently in both Bun and Node.js runtimes.
2926
+ *
2927
+ * @returns A promise that resolves when the server has been stopped.
2928
+ *
2929
+ * @example
2930
+ * ```typescript
2931
+ * const app = new Shokupan();
2932
+ * const server = await app.listen(3000);
2933
+ *
2934
+ * // Later, when you want to stop the server
2935
+ * await app.stop();
2936
+ * ```
2937
+ * @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
2938
+ */
2939
+ async stop(closeActiveConnections) {
2940
+ if (this.cpuMonitor) {
2941
+ this.cpuMonitor.stop();
2942
+ this.cpuMonitor = void 0;
2943
+ }
2944
+ if (this.server) {
2945
+ await this.server.stop(closeActiveConnections);
2946
+ this.server = void 0;
2947
+ }
2800
2948
  }
2801
2949
  [$dispatch](req) {
2802
2950
  return this.fetch(req);
@@ -2860,19 +3008,19 @@ class Shokupan extends ShokupanRouter {
2860
3008
  "http.method": req.method
2861
3009
  }
2862
3010
  };
2863
- const parent = store?.get("span");
3011
+ const parent = store?.span;
2864
3012
  const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
2865
3013
  return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2866
- const ctxMap = /* @__PURE__ */ new Map();
2867
- ctxMap.set("span", span);
2868
- ctxMap.set("request", req);
2869
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
3014
+ const ctxStore = new RequestContextStore();
3015
+ ctxStore.span = span;
3016
+ ctxStore.request = req;
3017
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server).finally(() => span.end()));
2870
3018
  });
2871
3019
  }
2872
3020
  if (this.applicationConfig.enableAsyncLocalStorage) {
2873
- const ctxMap = /* @__PURE__ */ new Map();
2874
- ctxMap.set("request", req);
2875
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
3021
+ const ctxStore = new RequestContextStore();
3022
+ ctxStore.request = req;
3023
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server));
2876
3024
  }
2877
3025
  return this.handleRequest(req, server);
2878
3026
  }
@@ -2894,24 +3042,34 @@ class Shokupan extends ShokupanRouter {
2894
3042
  const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2895
3043
  const match = this.find(req.method, ctx.path);
2896
3044
  if (match) {
3045
+ ctx[$routeMatched] = true;
2897
3046
  ctx.params = match.params;
2898
3047
  await bodyParsing;
2899
3048
  return match.handler(ctx);
2900
3049
  }
3050
+ if (ctx.upgrade()) {
3051
+ return void 0;
3052
+ }
2901
3053
  return null;
2902
3054
  });
2903
3055
  let response;
2904
3056
  if (result instanceof Response) {
2905
3057
  response = result;
2906
- } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2907
- response = ctx._finalResponse;
3058
+ } else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
3059
+ response = ctx[$finalResponse];
2908
3060
  } else if (result === null || result === void 0) {
2909
- if (ctx._finalResponse instanceof Response) {
2910
- response = ctx._finalResponse;
2911
- } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
3061
+ if (ctx[$finalResponse] instanceof Response) {
3062
+ response = ctx[$finalResponse];
3063
+ } else if (ctx.isUpgraded) {
3064
+ return void 0;
3065
+ } else if (ctx[$routeMatched]) {
2912
3066
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2913
3067
  } else {
2914
- response = ctx.text("Not Found", 404);
3068
+ if (ctx.response.status !== HTTP_STATUS.OK) {
3069
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3070
+ } else {
3071
+ response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
3072
+ }
2915
3073
  }
2916
3074
  } else if (typeof result === "object") {
2917
3075
  response = ctx.json(result);
@@ -2922,10 +3080,9 @@ class Shokupan extends ShokupanRouter {
2922
3080
  await this.runHooks("onResponseStart", ctx, response);
2923
3081
  return response;
2924
3082
  } catch (err) {
2925
- console.error(err);
2926
- const span = asyncContext.getStore()?.get("span");
3083
+ const span = asyncContext.getStore()?.span;
2927
3084
  if (span) span.setStatus({ code: 2 });
2928
- const status = err.status || err.statusCode || 500;
3085
+ const status = getErrorStatus(err);
2929
3086
  const body = { error: err.message || "Internal Server Error" };
2930
3087
  if (err.errors) body.errors = err.errors;
2931
3088
  await this.runHooks("onError", ctx, err);
@@ -2947,16 +3104,188 @@ class Shokupan extends ShokupanRouter {
2947
3104
  }
2948
3105
  return executionPromise.catch((err) => {
2949
3106
  if (err.message === "Request Timeout") {
2950
- return ctx.text("Request Timeout", 408);
3107
+ return ctx.text("Request Timeout", HTTP_STATUS.REQUEST_TIMEOUT);
2951
3108
  }
2952
3109
  console.error("Unexpected error in request execution:", err);
2953
- return ctx.text("Internal Server Error", 500);
3110
+ return ctx.text("Internal Server Error", HTTP_STATUS.INTERNAL_SERVER_ERROR);
2954
3111
  }).then(async (res) => {
2955
3112
  await this.runHooks("onResponseEnd", ctx, res);
2956
3113
  return res;
2957
3114
  });
2958
3115
  }
2959
3116
  }
3117
+ function RateLimitMiddleware(options = {}) {
3118
+ const windowMs = options.windowMs || 60 * 1e3;
3119
+ const max = options.limit || options.max || 5;
3120
+ const message = options.message || "Too many requests, please try again later.";
3121
+ const statusCode = options.statusCode || 429;
3122
+ const headers = options.headers !== false;
3123
+ const mode = options.mode || "user";
3124
+ const trustedProxies = options.trustedProxies || [];
3125
+ const keyGenerator = options.keyGenerator || ((ctx) => {
3126
+ if (mode === "absolute") {
3127
+ return "global";
3128
+ }
3129
+ const xForwardedFor = ctx.headers.get("x-forwarded-for");
3130
+ if (xForwardedFor && trustedProxies.length > 0) {
3131
+ const ips = xForwardedFor.split(",").map((ip) => ip.trim());
3132
+ for (let i = ips.length - 1; i >= 0; i--) {
3133
+ const ip = ips[i];
3134
+ if (!trustedProxies.includes(ip)) {
3135
+ if (/^[\d.:a-fA-F]+$/.test(ip)) {
3136
+ return ip;
3137
+ }
3138
+ }
3139
+ }
3140
+ }
3141
+ return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
3142
+ });
3143
+ const skip = options.skip || (() => false);
3144
+ const hits = /* @__PURE__ */ new Map();
3145
+ const interval = setInterval(() => {
3146
+ const now = Date.now();
3147
+ const entries = Array.from(hits.entries());
3148
+ for (let i = 0; i < entries.length; i++) {
3149
+ const [key, record] = entries[i];
3150
+ if (record.resetTime <= now) {
3151
+ hits.delete(key);
3152
+ }
3153
+ }
3154
+ }, windowMs);
3155
+ if (interval.unref) interval.unref();
3156
+ const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
3157
+ if (skip(ctx)) return next();
3158
+ const key = keyGenerator(ctx);
3159
+ const now = Date.now();
3160
+ let record = hits.get(key);
3161
+ if (!record || record.resetTime <= now) {
3162
+ record = {
3163
+ hits: 0,
3164
+ resetTime: now + windowMs
3165
+ };
3166
+ hits.set(key, record);
3167
+ }
3168
+ record.hits++;
3169
+ const remaining = Math.max(0, max - record.hits);
3170
+ const resetTime = Math.ceil(record.resetTime / 1e3);
3171
+ const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
3172
+ const setHeaders = (res) => {
3173
+ if (!headers || !res || !res.headers) return;
3174
+ try {
3175
+ res.headers.set("X-RateLimit-Limit", String(max));
3176
+ res.headers.set("X-RateLimit-Remaining", String(remaining));
3177
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
3178
+ } catch (e) {
3179
+ }
3180
+ };
3181
+ if (record.hits > max) {
3182
+ if (options.onRateLimited) {
3183
+ const result = await options.onRateLimited(ctx, key);
3184
+ if (result instanceof Response) {
3185
+ return result;
3186
+ }
3187
+ }
3188
+ 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);
3191
+ if (headers) {
3192
+ setHeaders(res);
3193
+ res.headers.set("Retry-After", String(retryAfter));
3194
+ }
3195
+ return res;
3196
+ }
3197
+ const response = await next();
3198
+ if (response instanceof Response && headers) {
3199
+ setHeaders(response);
3200
+ }
3201
+ return response;
3202
+ };
3203
+ rateLimitMiddleware.isBuiltin = true;
3204
+ rateLimitMiddleware.pluginName = "RateLimit";
3205
+ return rateLimitMiddleware;
3206
+ }
3207
+ function Controller(path2 = "/") {
3208
+ return (target) => {
3209
+ target[$controllerPath] = path2;
3210
+ };
3211
+ }
3212
+ function Use(...middleware) {
3213
+ return (target, propertyKey, descriptor) => {
3214
+ if (!propertyKey) {
3215
+ const existing = target[$middleware] || [];
3216
+ target[$middleware] = [...existing, ...middleware];
3217
+ } else {
3218
+ if (!target[$middleware]) {
3219
+ target[$middleware] = /* @__PURE__ */ new Map();
3220
+ }
3221
+ const existing = target[$middleware].get(propertyKey) || [];
3222
+ target[$middleware].set(propertyKey, [...existing, ...middleware]);
3223
+ }
3224
+ };
3225
+ }
3226
+ function createParamDecorator(type) {
3227
+ return (name) => {
3228
+ return (target, propertyKey, parameterIndex) => {
3229
+ if (!target[$routeArgs]) {
3230
+ target[$routeArgs] = /* @__PURE__ */ new Map();
3231
+ }
3232
+ if (!target[$routeArgs].has(propertyKey)) {
3233
+ target[$routeArgs].set(propertyKey, []);
3234
+ }
3235
+ target[$routeArgs].get(propertyKey).push({
3236
+ index: parameterIndex,
3237
+ type,
3238
+ name
3239
+ });
3240
+ };
3241
+ };
3242
+ }
3243
+ const Body = createParamDecorator(RouteParamType.BODY);
3244
+ const Param = createParamDecorator(RouteParamType.PARAM);
3245
+ const Query = createParamDecorator(RouteParamType.QUERY);
3246
+ const Headers$1 = createParamDecorator(RouteParamType.HEADER);
3247
+ const Req = createParamDecorator(RouteParamType.REQUEST);
3248
+ const Ctx = createParamDecorator(RouteParamType.CONTEXT);
3249
+ function Spec(spec) {
3250
+ return (target, propertyKey, descriptor) => {
3251
+ if (!target[$routeSpec]) {
3252
+ target[$routeSpec] = /* @__PURE__ */ new Map();
3253
+ }
3254
+ target[$routeSpec].set(propertyKey, spec);
3255
+ };
3256
+ }
3257
+ function createMethodDecorator(method) {
3258
+ return (path2 = "/") => {
3259
+ return (target, propertyKey, descriptor) => {
3260
+ if (!target[$routeMethods]) {
3261
+ target[$routeMethods] = /* @__PURE__ */ new Map();
3262
+ }
3263
+ target[$routeMethods].set(propertyKey, {
3264
+ method,
3265
+ path: path2
3266
+ });
3267
+ };
3268
+ };
3269
+ }
3270
+ const Get = createMethodDecorator("GET");
3271
+ const Post = createMethodDecorator("POST");
3272
+ const Put = createMethodDecorator("PUT");
3273
+ const Delete = createMethodDecorator("DELETE");
3274
+ const Patch = createMethodDecorator("PATCH");
3275
+ const Options = createMethodDecorator("OPTIONS");
3276
+ const Head = createMethodDecorator("HEAD");
3277
+ const All = createMethodDecorator("ALL");
3278
+ function Event(eventName) {
3279
+ return (target, propertyKey, descriptor) => {
3280
+ target[$eventMethods] ??= /* @__PURE__ */ new Map();
3281
+ target[$eventMethods].set(propertyKey, {
3282
+ eventName
3283
+ });
3284
+ };
3285
+ }
3286
+ function RateLimit(options) {
3287
+ return Use(RateLimitMiddleware(options));
3288
+ }
2960
3289
  class AuthPlugin extends ShokupanRouter {
2961
3290
  constructor(authConfig) {
2962
3291
  super();
@@ -2965,6 +3294,13 @@ class AuthPlugin extends ShokupanRouter {
2965
3294
  this.init();
2966
3295
  }
2967
3296
  secret;
3297
+ onInit(app, options) {
3298
+ if (options?.path) {
3299
+ app.mount(options.path, this);
3300
+ } else {
3301
+ app.mount(options.path ?? "/", this);
3302
+ }
3303
+ }
2968
3304
  getProviderInstance(name, p) {
2969
3305
  switch (name) {
2970
3306
  case "github":
@@ -3117,73 +3453,809 @@ class AuthPlugin extends ShokupanRouter {
3117
3453
  provider,
3118
3454
  raw: data
3119
3455
  };
3120
- } else if (provider === "auth0" || provider === "okta") {
3121
- const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
3122
- const endpoint = provider === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
3123
- const res = await fetch(endpoint, {
3124
- headers: { Authorization: `Bearer ${token}` }
3456
+ } else if (provider === "auth0" || provider === "okta") {
3457
+ const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
3458
+ const endpoint = provider === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
3459
+ const res = await fetch(endpoint, {
3460
+ headers: { Authorization: `Bearer ${token}` }
3461
+ });
3462
+ const data = await res.json();
3463
+ user = {
3464
+ id: data.sub,
3465
+ name: data.name,
3466
+ email: data.email,
3467
+ picture: data.picture,
3468
+ provider,
3469
+ raw: data
3470
+ };
3471
+ } else if (provider === "apple") {
3472
+ if (idToken) {
3473
+ const payload = jose__namespace.decodeJwt(idToken);
3474
+ user = {
3475
+ id: payload.sub,
3476
+ email: payload["email"],
3477
+ provider,
3478
+ raw: payload
3479
+ };
3480
+ }
3481
+ } else if (provider === "oauth2") {
3482
+ if (config.userInfoUrl) {
3483
+ const res = await fetch(config.userInfoUrl, {
3484
+ headers: { Authorization: `Bearer ${token}` }
3485
+ });
3486
+ const data = await res.json();
3487
+ user = {
3488
+ id: data.id || data.sub || "unknown",
3489
+ name: data.name,
3490
+ email: data.email,
3491
+ picture: data.picture,
3492
+ provider,
3493
+ raw: data
3494
+ };
3495
+ }
3496
+ }
3497
+ return user;
3498
+ }
3499
+ /**
3500
+ * Middleware to verify JWT
3501
+ */
3502
+ getMiddleware() {
3503
+ return async (ctx, next) => {
3504
+ const authHeader = ctx.req.headers.get("Authorization");
3505
+ let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
3506
+ if (!token) {
3507
+ const cookieHeader = ctx.req.headers.get("Cookie");
3508
+ token = cookieHeader?.match(/auth_token=([^;]+)/)?.[1] || null;
3509
+ }
3510
+ if (token) {
3511
+ try {
3512
+ const { payload } = await jose__namespace.jwtVerify(token, this.secret);
3513
+ ctx.user = payload;
3514
+ } catch {
3515
+ }
3516
+ }
3517
+ return next();
3518
+ };
3519
+ }
3520
+ }
3521
+ class ClusterPlugin {
3522
+ constructor(options = {}) {
3523
+ this.options = options;
3524
+ }
3525
+ onInit(app) {
3526
+ const originalListen = app.listen.bind(app);
3527
+ const { workers = "auto", silent = false, sticky = false } = this.options;
3528
+ const isBun = typeof Bun !== "undefined";
3529
+ const numCPUs = os.cpus().length;
3530
+ const numWorkers = workers === "auto" || workers === -1 ? numCPUs : workers;
3531
+ if (numWorkers <= 1) {
3532
+ return;
3533
+ }
3534
+ app.listen = async (port) => {
3535
+ const finalPort = port ?? app.applicationConfig.port ?? 3e3;
3536
+ if (isBun) {
3537
+ return this.handleBun(app, finalPort, numWorkers, originalListen);
3538
+ } else {
3539
+ return this.handleNode(app, finalPort, numWorkers, originalListen, silent, sticky);
3540
+ }
3541
+ };
3542
+ }
3543
+ async handleBun(app, port, workers, originalListen) {
3544
+ const workerId = process.env["SHOKUPAN_WORKER_ID"];
3545
+ if (workerId) {
3546
+ app.applicationConfig.reusePort = true;
3547
+ return originalListen(port);
3548
+ }
3549
+ console.log(`[Cluster] Starting ${workers} Bun workers on port ${port}...`);
3550
+ const spawnWorker = (id) => {
3551
+ Bun.spawn([process.argv0, ...process.argv.slice(1)], {
3552
+ env: { ...process.env, SHOKUPAN_WORKER_ID: id },
3553
+ stdio: ["inherit", "inherit", "inherit"],
3554
+ onExit(proc, exitCode, signalCode, error) {
3555
+ console.log(`[Cluster] Worker ${id} died (code: ${exitCode}). Restarting...`);
3556
+ spawnWorker(id);
3557
+ }
3558
+ });
3559
+ };
3560
+ for (let i = 0; i < workers; i++) {
3561
+ spawnWorker(process.pid + "_" + i + 1);
3562
+ }
3563
+ setInterval(() => {
3564
+ }, 1e3 * 60 * 60);
3565
+ return {
3566
+ stop: () => {
3567
+ },
3568
+ port
3569
+ };
3570
+ }
3571
+ async handleNode(app, port, workers, originalListen, silent, sticky) {
3572
+ if (cluster.isPrimary) {
3573
+ console.log(`[Cluster] Master ${process.pid} is running`);
3574
+ const fork = () => cluster.fork(process.env);
3575
+ for (let i = 0; i < workers; i++) {
3576
+ fork();
3577
+ }
3578
+ cluster.on("exit", (worker, code, signal) => {
3579
+ console.log(`[Cluster] Worker ${worker.process.pid} died. Restarting...`);
3580
+ fork();
3581
+ });
3582
+ if (sticky) {
3583
+ const server = net.createServer({ pauseOnConnect: true }, (connection) => {
3584
+ const remote = connection.remoteAddress || "";
3585
+ let hash = 0;
3586
+ for (let i = 0; i < remote.length; i++) {
3587
+ hash = (hash << 5) - hash + remote.charCodeAt(i);
3588
+ hash |= 0;
3589
+ }
3590
+ const index = Math.abs(hash) % workers;
3591
+ const worker = Object.values(cluster.workers)[index];
3592
+ if (worker) {
3593
+ worker.send("sticky-session:connection", connection);
3594
+ } else {
3595
+ connection.end();
3596
+ }
3597
+ });
3598
+ server.listen(port, () => {
3599
+ console.log(`[Cluster] Sticky Load Balancer listening on port ${port}`);
3600
+ });
3601
+ return {
3602
+ close: () => server.close(),
3603
+ port
3604
+ };
3605
+ } else {
3606
+ return {
3607
+ close: () => {
3608
+ },
3609
+ // Master controls
3610
+ port
3611
+ };
3612
+ }
3613
+ } else {
3614
+ if (sticky) {
3615
+ const server = await originalListen(0);
3616
+ process.on("message", (message, handle) => {
3617
+ if (message !== "sticky-session:connection") return;
3618
+ if (!handle) return;
3619
+ server.emit("connection", handle);
3620
+ handle.resume();
3621
+ });
3622
+ return server;
3623
+ } else {
3624
+ return originalListen(port);
3625
+ }
3626
+ }
3627
+ }
3628
+ }
3629
+ const INTERVALS = [
3630
+ { label: "10s", ms: 10 * 1e3 },
3631
+ { label: "1m", ms: 60 * 1e3 },
3632
+ { label: "5m", ms: 5 * 60 * 1e3 },
3633
+ { label: "1h", ms: 60 * 60 * 1e3 },
3634
+ { label: "2h", ms: 2 * 60 * 60 * 1e3 },
3635
+ { label: "6h", ms: 6 * 60 * 60 * 1e3 },
3636
+ { label: "12h", ms: 12 * 60 * 60 * 1e3 },
3637
+ { label: "1d", ms: 24 * 60 * 60 * 1e3 },
3638
+ { label: "3d", ms: 3 * 24 * 60 * 60 * 1e3 },
3639
+ { label: "7d", ms: 7 * 24 * 60 * 60 * 1e3 },
3640
+ { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
3641
+ ];
3642
+ class MetricsCollector {
3643
+ currentIntervalStart = {};
3644
+ pendingDetails = {};
3645
+ eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
3646
+ timer = null;
3647
+ constructor() {
3648
+ this.eventLoopHistogram.enable();
3649
+ const now = Date.now();
3650
+ INTERVALS.forEach((int) => {
3651
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3652
+ this.pendingDetails[int.label] = [];
3653
+ });
3654
+ this.timer = setInterval(() => this.collect(), 1e4);
3655
+ }
3656
+ recordRequest(duration, isError) {
3657
+ INTERVALS.forEach((int) => {
3658
+ this.pendingDetails[int.label].push({ duration, isError });
3659
+ });
3660
+ }
3661
+ alignTimestamp(ts, intervalMs) {
3662
+ return Math.floor(ts / intervalMs) * intervalMs;
3663
+ }
3664
+ async collect() {
3665
+ try {
3666
+ const now = Date.now();
3667
+ console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
3668
+ for (const int of INTERVALS) {
3669
+ const start = this.currentIntervalStart[int.label];
3670
+ if (now >= start + int.ms) {
3671
+ console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
3672
+ await this.flushInterval(int.label, start, int.ms);
3673
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3674
+ }
3675
+ }
3676
+ } catch (error) {
3677
+ console.error("[MetricsCollector] Error in collect():", error);
3678
+ }
3679
+ }
3680
+ async flushInterval(label, timestamp, durationMs) {
3681
+ const reqs = this.pendingDetails[label];
3682
+ console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
3683
+ this.pendingDetails[label] = [];
3684
+ if (reqs.length === 0) {
3685
+ console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
3686
+ return;
3687
+ }
3688
+ const totalReqs = reqs.length;
3689
+ const errorReqs = reqs.filter((r) => r.isError).length;
3690
+ const successReqs = totalReqs - errorReqs;
3691
+ const duratons = reqs.map((r) => r.duration).sort((a, b) => a - b);
3692
+ const rps = totalReqs / (durationMs / 1e3);
3693
+ const sum = duratons.reduce((a, b) => a + b, 0);
3694
+ const avg = totalReqs > 0 ? sum / totalReqs : 0;
3695
+ const getP = (p) => {
3696
+ if (duratons.length === 0) return 0;
3697
+ const idx = Math.floor(duratons.length * p);
3698
+ return duratons[idx];
3699
+ };
3700
+ const metric = {
3701
+ timestamp,
3702
+ interval: label,
3703
+ cpu: os__namespace.loadavg()[0],
3704
+ // Using load avg for simplicity as per requirements (Load)
3705
+ load: os__namespace.loadavg(),
3706
+ memory: {
3707
+ used: process.memoryUsage().rss,
3708
+ total: os__namespace.totalmem(),
3709
+ heapUsed: process.memoryUsage().heapUsed,
3710
+ heapTotal: process.memoryUsage().heapTotal
3711
+ },
3712
+ eventLoopLatency: {
3713
+ min: this.eventLoopHistogram.min / 1e6,
3714
+ max: this.eventLoopHistogram.max / 1e6,
3715
+ mean: this.eventLoopHistogram.mean / 1e6,
3716
+ p50: this.eventLoopHistogram.percentile(50) / 1e6,
3717
+ p95: this.eventLoopHistogram.percentile(95) / 1e6,
3718
+ p99: this.eventLoopHistogram.percentile(99) / 1e6
3719
+ },
3720
+ requests: {
3721
+ total: totalReqs,
3722
+ rps,
3723
+ success: successReqs,
3724
+ error: errorReqs
3725
+ },
3726
+ responseTime: {
3727
+ min: duratons[0] || 0,
3728
+ max: duratons[duratons.length - 1] || 0,
3729
+ avg,
3730
+ p50: getP(0.5),
3731
+ p95: getP(0.95),
3732
+ p99: getP(0.99)
3733
+ }
3734
+ };
3735
+ console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
3736
+ try {
3737
+ 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`);
3744
+ } catch (e) {
3745
+ console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
3746
+ }
3747
+ }
3748
+ // Cleanup if needed
3749
+ stop() {
3750
+ if (this.timer) clearInterval(this.timer);
3751
+ this.eventLoopHistogram.disable();
3752
+ }
3753
+ }
3754
+ class Collector {
3755
+ constructor(dashboard) {
3756
+ this.dashboard = dashboard;
3757
+ }
3758
+ currentNode;
3759
+ trackStep(id, type, duration, status, error) {
3760
+ if (!id) return;
3761
+ this.dashboard.recordNodeMetric(id, type, duration, status === "error");
3762
+ }
3763
+ trackEdge(fromId, toId) {
3764
+ if (!fromId || !toId) return;
3765
+ this.dashboard.recordEdgeMetric(fromId, toId);
3766
+ }
3767
+ setNode(id) {
3768
+ this.currentNode = id;
3769
+ }
3770
+ getCurrentNode() {
3771
+ return this.currentNode;
3772
+ }
3773
+ }
3774
+ class Dashboard {
3775
+ constructor(dashboardConfig = {}) {
3776
+ this.dashboardConfig = dashboardConfig;
3777
+ }
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();
3788
+ metrics = {
3789
+ totalRequests: 0,
3790
+ successfulRequests: 0,
3791
+ failedRequests: 0,
3792
+ activeRequests: 0,
3793
+ averageTotalTime_ms: 0,
3794
+ recentTimings: [],
3795
+ logs: [],
3796
+ rateLimitedCounts: {},
3797
+ nodeMetrics: {},
3798
+ edgeMetrics: {}
3799
+ };
3800
+ eta = new eta$2.Eta({
3801
+ views: Dashboard.getBasePath() + "/static",
3802
+ cache: false
3803
+ });
3804
+ startTime = Date.now();
3805
+ instrumented = false;
3806
+ metricsCollector = new MetricsCollector();
3807
+ // ShokupanPlugin interface implementation
3808
+ onInit(app, options) {
3809
+ this[$appRoot] = app;
3810
+ const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
3811
+ const hooks = this.getHooks();
3812
+ if (!app.middleware) {
3813
+ app.middleware = [];
3814
+ }
3815
+ const hooksMiddleware = async (ctx, next) => {
3816
+ if (hooks.onRequestStart) {
3817
+ await hooks.onRequestStart(ctx);
3818
+ }
3819
+ await next();
3820
+ if (hooks.onResponseEnd) {
3821
+ const effectiveResponse = ctx._finalResponse || ctx.response || {};
3822
+ await hooks.onResponseEnd(ctx, effectiveResponse);
3823
+ }
3824
+ };
3825
+ app.use(hooksMiddleware);
3826
+ app.mount(mountPath, this.router);
3827
+ this.setupRoutes();
3828
+ }
3829
+ setupRoutes() {
3830
+ this.router.get("/metrics", async (ctx) => {
3831
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
3832
+ const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
3833
+ const interval = ctx.query["interval"];
3834
+ if (interval) {
3835
+ const intervalMap = {
3836
+ "10s": 10 * 1e3,
3837
+ "1m": 60 * 1e3,
3838
+ "5m": 5 * 60 * 1e3,
3839
+ "30m": 30 * 60 * 1e3,
3840
+ "1h": 60 * 60 * 1e3,
3841
+ "2h": 2 * 60 * 60 * 1e3,
3842
+ "6h": 6 * 60 * 60 * 1e3,
3843
+ "12h": 12 * 60 * 60 * 1e3,
3844
+ "1d": 24 * 60 * 60 * 1e3,
3845
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3846
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3847
+ "30d": 30 * 24 * 60 * 60 * 1e3
3848
+ };
3849
+ const ms = intervalMap[interval] || 60 * 1e3;
3850
+ const startTime = Date.now() - ms;
3851
+ let stats;
3852
+ try {
3853
+ stats = await datastore.query(`
3854
+ SELECT
3855
+ count() as total,
3856
+ count(IF status < 400 THEN 1 END) as success,
3857
+ count(IF status >= 400 THEN 1 END) as failed,
3858
+ math::mean(duration) as avg_latency
3859
+ FROM requests
3860
+ WHERE timestamp >= $start
3861
+ GROUP ALL
3862
+ `, { start: startTime });
3863
+ } catch (error) {
3864
+ console.error("[Dashboard] Query failed at plugin.ts:180-191", {
3865
+ error,
3866
+ interval,
3867
+ startTime,
3868
+ query: "metrics interval stats",
3869
+ stack: new Error().stack
3870
+ });
3871
+ throw error;
3872
+ }
3873
+ const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
3874
+ return ctx.json({
3875
+ metrics: {
3876
+ totalRequests: s.total || 0,
3877
+ successfulRequests: s.success || 0,
3878
+ failedRequests: s.failed || 0,
3879
+ activeRequests: this.metrics.activeRequests,
3880
+ averageTotalTime_ms: s.avg_latency || 0,
3881
+ recentTimings: this.metrics.recentTimings,
3882
+ logs: [],
3883
+ rateLimitedCounts: this.metrics.rateLimitedCounts,
3884
+ nodeMetrics: this.metrics.nodeMetrics,
3885
+ edgeMetrics: this.metrics.edgeMetrics
3886
+ },
3887
+ uptime
3888
+ });
3889
+ }
3890
+ return ctx.json({
3891
+ metrics: this.metrics,
3892
+ uptime
3893
+ });
3894
+ });
3895
+ this.router.get("/metrics/history", async (ctx) => {
3896
+ const interval = ctx.query["interval"] || "1m";
3897
+ const intervalMap = {
3898
+ "10s": 10 * 1e3,
3899
+ "1m": 60 * 1e3,
3900
+ "5m": 5 * 60 * 1e3,
3901
+ "30m": 30 * 60 * 1e3,
3902
+ "1h": 60 * 60 * 1e3,
3903
+ "2h": 2 * 60 * 60 * 1e3,
3904
+ "6h": 6 * 60 * 60 * 1e3,
3905
+ "12h": 12 * 60 * 60 * 1e3,
3906
+ "1d": 24 * 60 * 60 * 1e3,
3907
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3908
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3909
+ "30d": 30 * 24 * 60 * 60 * 1e3
3910
+ };
3911
+ const periodMs = intervalMap[interval] || 60 * 1e3;
3912
+ const startTime = Date.now() - periodMs * 3;
3913
+ const endTime = Date.now();
3914
+ const result = await datastore.query(
3915
+ "SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
3916
+ { start: startTime, end: endTime, interval }
3917
+ );
3918
+ return ctx.json({
3919
+ metrics: result[0] || []
3125
3920
  });
3126
- const data = await res.json();
3127
- user = {
3128
- id: data.sub,
3129
- name: data.name,
3130
- email: data.email,
3131
- picture: data.picture,
3132
- provider,
3133
- raw: data
3921
+ });
3922
+ const getIntervalStartTime = (interval) => {
3923
+ if (!interval) return 0;
3924
+ const intervalMap = {
3925
+ "10s": 10 * 1e3,
3926
+ "1m": 60 * 1e3,
3927
+ "5m": 5 * 60 * 1e3,
3928
+ "30m": 30 * 60 * 1e3,
3929
+ "1h": 60 * 60 * 1e3,
3930
+ "2h": 2 * 60 * 60 * 1e3,
3931
+ "6h": 6 * 60 * 60 * 1e3,
3932
+ "12h": 12 * 60 * 60 * 1e3,
3933
+ "1d": 24 * 60 * 60 * 1e3,
3934
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3935
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3936
+ "30d": 30 * 24 * 60 * 60 * 1e3
3134
3937
  };
3135
- } else if (provider === "apple") {
3136
- if (idToken) {
3137
- const payload = jose__namespace.decodeJwt(idToken);
3138
- user = {
3139
- id: payload.sub,
3140
- email: payload["email"],
3141
- provider,
3142
- raw: payload
3143
- };
3938
+ const ms = intervalMap[interval] || 0;
3939
+ return ms ? Date.now() - ms : 0;
3940
+ };
3941
+ this.router.get("/requests/top", async (ctx) => {
3942
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3943
+ const result = await datastore.query(
3944
+ "SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3945
+ { start: startTime }
3946
+ );
3947
+ return ctx.json({ top: result[0] || [] });
3948
+ });
3949
+ this.router.get("/errors/top", async (ctx) => {
3950
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3951
+ const result = await datastore.query(
3952
+ "SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
3953
+ { start: startTime }
3954
+ );
3955
+ return ctx.json({ top: result[0] || [] });
3956
+ });
3957
+ this.router.get("/requests/failing", async (ctx) => {
3958
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3959
+ const result = await datastore.query(
3960
+ "SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3961
+ { start: startTime }
3962
+ );
3963
+ return ctx.json({ top: result[0] || [] });
3964
+ });
3965
+ this.router.get("/requests/slowest", async (ctx) => {
3966
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3967
+ const result = await datastore.query(
3968
+ "SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
3969
+ { start: startTime }
3970
+ );
3971
+ return ctx.json({ slowest: result[0] || [] });
3972
+ });
3973
+ this.router.get("/registry", (ctx) => {
3974
+ const app = this[$appRoot];
3975
+ if (!this.instrumented && app) {
3976
+ this.instrumentApp(app);
3144
3977
  }
3145
- } else if (provider === "oauth2") {
3146
- if (config.userInfoUrl) {
3147
- const res = await fetch(config.userInfoUrl, {
3148
- headers: { Authorization: `Bearer ${token}` }
3978
+ const registry = app?.getComponentRegistry?.();
3979
+ if (registry) {
3980
+ this.assignIdsToRegistry(registry, "root");
3981
+ }
3982
+ return ctx.json({ registry: registry || {} });
3983
+ });
3984
+ this.router.get("/requests", async (ctx) => {
3985
+ const result = await datastore.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
3986
+ return ctx.json({ requests: result[0] || [] });
3987
+ });
3988
+ this.router.get("/requests/:id", async (ctx) => {
3989
+ const result = await datastore.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
3990
+ return ctx.json({ request: result[0]?.[0] });
3991
+ });
3992
+ this.router.get("/failures", async (ctx) => {
3993
+ const result = await datastore.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
3994
+ return ctx.json({ failures: result[0] });
3995
+ });
3996
+ this.router.post("/replay", async (ctx) => {
3997
+ const body = await ctx.body();
3998
+ const app = this[$appRoot];
3999
+ if (!app) return unknownError(ctx);
4000
+ try {
4001
+ const result = await app.processRequest({
4002
+ method: body.method,
4003
+ path: body.url,
4004
+ // or path
4005
+ headers: body.headers,
4006
+ body: body.body
3149
4007
  });
3150
- const data = await res.json();
3151
- user = {
3152
- id: data.id || data.sub || "unknown",
3153
- name: data.name,
3154
- email: data.email,
3155
- picture: data.picture,
3156
- provider,
3157
- raw: data
3158
- };
4008
+ return ctx.json({
4009
+ status: result.status,
4010
+ headers: result.headers,
4011
+ data: result.data
4012
+ });
4013
+ } catch (e) {
4014
+ return ctx.json({ error: String(e) }, 500);
3159
4015
  }
4016
+ });
4017
+ this.router.get("/", async (ctx) => {
4018
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
4019
+ 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, {
4023
+ metrics: this.metrics,
4024
+ uptime,
4025
+ rootPath: process.cwd(),
4026
+ linkPattern,
4027
+ headers: this.dashboardConfig.getRequestHeaders?.()
4028
+ }));
4029
+ });
4030
+ }
4031
+ instrumentApp(app) {
4032
+ if (!app.getComponentRegistry) return;
4033
+ const registry = app.getComponentRegistry();
4034
+ this.assignIdsToRegistry(registry, "root");
4035
+ this.instrumented = true;
4036
+ }
4037
+ // Traverses registry, generates IDs, and attaches them to the actual function objects
4038
+ assignIdsToRegistry(node, parentId) {
4039
+ if (!node) return;
4040
+ const makeId = (type, parent, idx, name) => `${type}_${parent}_${idx}_${name.replace(/[^a-zA-Z0-9]/g, "")}`;
4041
+ node.middleware?.forEach((mw, idx) => {
4042
+ const id = makeId("mw", parentId, idx, mw.name);
4043
+ mw.id = id;
4044
+ if (mw._fn) mw._fn._debugId = id;
4045
+ });
4046
+ node.controllers?.forEach((ctrl, idx) => {
4047
+ const id = makeId("ctrl", parentId, idx, ctrl.name);
4048
+ ctrl.id = id;
4049
+ });
4050
+ node.routes?.forEach((r, idx) => {
4051
+ const id = makeId("route", parentId, idx, r.handlerName || "handler");
4052
+ r.id = id;
4053
+ if (r._fn) r._fn._debugId = id;
4054
+ });
4055
+ node.routers?.forEach((r, idx) => {
4056
+ const id = makeId("router", parentId, idx, r.path);
4057
+ r.id = id;
4058
+ this.assignIdsToRegistry(r.children, id);
4059
+ });
4060
+ }
4061
+ recordNodeMetric(id, type, duration, isError) {
4062
+ if (!this.metrics.nodeMetrics[id]) {
4063
+ this.metrics.nodeMetrics[id] = {
4064
+ id,
4065
+ type,
4066
+ requests: 0,
4067
+ totalTime: 0,
4068
+ failures: 0,
4069
+ name: id
4070
+ // simplify
4071
+ };
3160
4072
  }
3161
- return user;
4073
+ const m = this.metrics.nodeMetrics[id];
4074
+ m.requests++;
4075
+ m.totalTime += duration;
4076
+ if (isError) m.failures++;
3162
4077
  }
3163
- /**
3164
- * Middleware to verify JWT
3165
- */
3166
- getMiddleware() {
3167
- return async (ctx, next) => {
3168
- const authHeader = ctx.req.headers.get("Authorization");
3169
- let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
3170
- if (!token) {
3171
- const cookieHeader = ctx.req.headers.get("Cookie");
3172
- token = cookieHeader?.match(/auth_token=([^;]+)/)?.[1] || null;
3173
- }
3174
- if (token) {
4078
+ recordEdgeMetric(from, to) {
4079
+ const key = `${from}|${to}`;
4080
+ this.metrics.edgeMetrics[key] = (this.metrics.edgeMetrics[key] || 0) + 1;
4081
+ }
4082
+ getLinkPattern() {
4083
+ const term = process.env["TERM_PROGRAM"] || "";
4084
+ if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
4085
+ return "vscode://file/{{absolute}}:{{line}}";
4086
+ }
4087
+ return "file:///{{absolute}}:{{line}}";
4088
+ }
4089
+ getHooks() {
4090
+ return {
4091
+ onRequestStart: (ctx) => {
4092
+ const app = this[$appRoot];
4093
+ if (!this.instrumented && app) {
4094
+ this.instrumentApp(app);
4095
+ }
4096
+ this.metrics.totalRequests++;
4097
+ this.metrics.activeRequests++;
4098
+ ctx._debugStartTime = performance.now();
4099
+ ctx[$debug] = new Collector(this);
4100
+ },
4101
+ onResponseEnd: async (ctx, response) => {
4102
+ this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
4103
+ const start = ctx._debugStartTime;
4104
+ let duration = 0;
4105
+ if (start) {
4106
+ duration = performance.now() - start;
4107
+ this.updateTiming(duration);
4108
+ }
4109
+ const isError = response.status >= 400;
4110
+ this.metricsCollector.recordRequest(duration, isError);
4111
+ if (response.status >= 400) {
4112
+ this.metrics.failedRequests++;
4113
+ if (response.status === 429) {
4114
+ const path2 = ctx.path;
4115
+ this.metrics.rateLimitedCounts[path2] = (this.metrics.rateLimitedCounts[path2] || 0) + 1;
4116
+ }
4117
+ try {
4118
+ const headers = {};
4119
+ if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
4120
+ ctx.request.headers.forEach((v, k) => {
4121
+ headers[k] = v;
4122
+ });
4123
+ }
4124
+ await datastore.set(new surrealdb.RecordId("failed_requests", ctx.requestId), {
4125
+ method: ctx.method,
4126
+ url: ctx.url.toString(),
4127
+ headers,
4128
+ status: response.status,
4129
+ timestamp: Date.now(),
4130
+ state: ctx.state
4131
+ // body?
4132
+ });
4133
+ } catch (e) {
4134
+ console.error("Failed to record failed request", e);
4135
+ }
4136
+ } else {
4137
+ this.metrics.successfulRequests++;
4138
+ }
4139
+ const logEntry = {
4140
+ method: ctx.method,
4141
+ url: ctx.url.toString(),
4142
+ status: response.status,
4143
+ duration,
4144
+ timestamp: Date.now(),
4145
+ handlerStack: ctx.handlerStack
4146
+ };
4147
+ this.metrics.logs.push(logEntry);
3175
4148
  try {
3176
- const { payload } = await jose__namespace.jwtVerify(token, this.secret);
3177
- ctx.user = payload;
3178
- } catch {
4149
+ await datastore.set(new surrealdb.RecordId("requests", ctx.requestId), logEntry);
4150
+ } catch (e) {
4151
+ console.error("Failed to record request log", e);
4152
+ }
4153
+ const retention = this.dashboardConfig.retentionMs ?? 72e5;
4154
+ const cutoff = Date.now() - retention;
4155
+ if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
4156
+ this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
3179
4157
  }
3180
4158
  }
3181
- return next();
3182
4159
  };
3183
4160
  }
4161
+ updateTiming(duration) {
4162
+ const alpha = 0.1;
4163
+ if (this.metrics.averageTotalTime_ms === 0) {
4164
+ this.metrics.averageTotalTime_ms = duration;
4165
+ } else {
4166
+ this.metrics.averageTotalTime_ms = alpha * duration + (1 - alpha) * this.metrics.averageTotalTime_ms;
4167
+ }
4168
+ this.metrics.recentTimings.push(duration);
4169
+ if (this.metrics.recentTimings.length > 50) {
4170
+ this.metrics.recentTimings.shift();
4171
+ }
4172
+ }
4173
+ }
4174
+ function unknownError(ctx) {
4175
+ return ctx.json({ error: "Unknown Error" }, 500);
4176
+ }
4177
+ const eta = new eta$2.Eta();
4178
+ class ScalarPlugin extends ShokupanRouter {
4179
+ constructor(pluginOptions = {}) {
4180
+ pluginOptions.config ??= {};
4181
+ super();
4182
+ this.pluginOptions = pluginOptions;
4183
+ this.init();
4184
+ }
4185
+ onInit(app, options) {
4186
+ if (options?.path) {
4187
+ app.mount(options.path, this);
4188
+ } else {
4189
+ app.mount(options.path ?? "/", this);
4190
+ }
4191
+ this.onMount(app);
4192
+ }
4193
+ init() {
4194
+ this.get("/", (ctx) => {
4195
+ let path2 = ctx.url.toString();
4196
+ if (!path2.endsWith("/")) path2 += "/";
4197
+ return ctx.html(eta.renderString(`<!doctype html>
4198
+ <html>
4199
+ <head>
4200
+ <title>API Reference</title>
4201
+ <meta charset = "utf-8" />
4202
+ <meta name="viewport" content = "width=device-width, initial-scale=1" />
4203
+ </head>
4204
+
4205
+ <body>
4206
+ <div id="app"></div>
4207
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
4208
+ <script>
4209
+ Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
4210
+ url: "<%= it.path %>openapi.json",
4211
+ }
4212
+ ])
4213
+ <\/script>
4214
+ </body>
4215
+
4216
+ </html>`, { path: path2, config: this.pluginOptions }));
4217
+ });
4218
+ this.get("/openapi.json", async (ctx) => {
4219
+ let spec;
4220
+ if (this.root.openApiSpec) {
4221
+ try {
4222
+ spec = structuredClone(this.root.openApiSpec);
4223
+ } catch (e) {
4224
+ spec = Object.assign({}, this.root.openApiSpec);
4225
+ }
4226
+ } else {
4227
+ spec = await (this.root || this).generateApiSpec();
4228
+ }
4229
+ if (this.pluginOptions.baseDocument) {
4230
+ deepMerge(spec, this.pluginOptions.baseDocument);
4231
+ }
4232
+ return ctx.json(spec);
4233
+ });
4234
+ }
4235
+ // New lifecycle method to be called by router.mount
4236
+ onMount(parent) {
4237
+ if (parent.onStart) {
4238
+ parent.onStart(async () => {
4239
+ if (this.pluginOptions.enableStaticAnalysis) {
4240
+ try {
4241
+ const entrypoint = process.argv[1];
4242
+ console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
4243
+ const analyzer$1 = new analyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
4244
+ let staticSpec = await analyzer$1.analyze();
4245
+ if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
4246
+ deepMerge(this.pluginOptions.baseDocument, staticSpec);
4247
+ console.log("[ScalarPlugin] Static analysis completed successfully.");
4248
+ } catch (err) {
4249
+ console.error("[ScalarPlugin] Failed to run static analysis:", err);
4250
+ }
4251
+ }
4252
+ });
4253
+ }
4254
+ }
3184
4255
  }
3185
4256
  function Compression(options = {}) {
3186
4257
  const threshold = options.threshold ?? 512;
4258
+ const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
3187
4259
  const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
3188
4260
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
3189
4261
  let method = null;
@@ -3196,24 +4268,27 @@ function Compression(options = {}) {
3196
4268
  } else if (acceptEncoding.includes("gzip")) method = "gzip";
3197
4269
  else if (acceptEncoding.includes("deflate")) method = "deflate";
3198
4270
  if (!method) return next();
4271
+ if (!allowedAlgorithms.has(method)) {
4272
+ return next();
4273
+ }
3199
4274
  let response = await next();
3200
- if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
3201
- response = ctx._finalResponse;
4275
+ if (!(response instanceof Response) && ctx[$finalResponse] instanceof Response) {
4276
+ response = ctx[$finalResponse];
3202
4277
  }
3203
4278
  if (response instanceof Response) {
3204
4279
  if (response.headers.has("Content-Encoding")) return response;
3205
4280
  let body;
3206
4281
  let bodySize;
3207
- if (ctx._rawBody !== void 0) {
3208
- if (typeof ctx._rawBody === "string") {
3209
- const encoded = new TextEncoder().encode(ctx._rawBody);
4282
+ if (ctx[$rawBody] !== void 0) {
4283
+ if (typeof ctx[$rawBody] === "string") {
4284
+ const encoded = new TextEncoder().encode(ctx[$rawBody]);
3210
4285
  body = encoded;
3211
4286
  bodySize = encoded.byteLength;
3212
- } else if (ctx._rawBody instanceof Uint8Array) {
3213
- body = ctx._rawBody;
3214
- bodySize = ctx._rawBody.byteLength;
4287
+ } else if (ctx[$rawBody] instanceof Uint8Array) {
4288
+ body = ctx[$rawBody];
4289
+ bodySize = ctx[$rawBody].byteLength;
3215
4290
  } else {
3216
- body = ctx._rawBody;
4291
+ body = ctx[$rawBody];
3217
4292
  bodySize = body.byteLength;
3218
4293
  }
3219
4294
  } else {
@@ -3750,77 +4825,6 @@ function enableOpenApiValidation(app) {
3750
4825
  precompileValidators(app, spec);
3751
4826
  });
3752
4827
  }
3753
- const eta = new eta$2.Eta();
3754
- class ScalarPlugin extends ShokupanRouter {
3755
- constructor(pluginOptions = {}) {
3756
- pluginOptions.config ??= {};
3757
- super();
3758
- this.pluginOptions = pluginOptions;
3759
- this.init();
3760
- }
3761
- init() {
3762
- this.get("/", (ctx) => {
3763
- let path2 = ctx.url.toString();
3764
- if (!path2.endsWith("/")) path2 += "/";
3765
- return ctx.html(eta.renderString(`<!doctype html>
3766
- <html>
3767
- <head>
3768
- <title>API Reference</title>
3769
- <meta charset = "utf-8" />
3770
- <meta name="viewport" content = "width=device-width, initial-scale=1" />
3771
- </head>
3772
-
3773
- <body>
3774
- <div id="app"></div>
3775
- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
3776
- <script>
3777
- Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3778
- url: "<%= it.path %>openapi.json",
3779
- }
3780
- ])
3781
- <\/script>
3782
- </body>
3783
-
3784
- </html>`, { path: path2, config: this.pluginOptions }));
3785
- });
3786
- this.get("/openapi.json", async (ctx) => {
3787
- let spec;
3788
- if (this.root.openApiSpec) {
3789
- try {
3790
- spec = structuredClone(this.root.openApiSpec);
3791
- } catch (e) {
3792
- spec = Object.assign({}, this.root.openApiSpec);
3793
- }
3794
- } else {
3795
- spec = await (this.root || this).generateApiSpec();
3796
- }
3797
- if (this.pluginOptions.baseDocument) {
3798
- deepMerge(spec, this.pluginOptions.baseDocument);
3799
- }
3800
- return ctx.json(spec);
3801
- });
3802
- }
3803
- // New lifecycle method to be called by router.mount
3804
- onMount(parent) {
3805
- if (parent.onStart) {
3806
- parent.onStart(async () => {
3807
- if (this.pluginOptions.enableStaticAnalysis) {
3808
- try {
3809
- const entrypoint = process.argv[1];
3810
- console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3811
- const analyzer = new openapiAnalyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
3812
- let staticSpec = await analyzer.analyze();
3813
- if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
3814
- deepMerge(this.pluginOptions.baseDocument, staticSpec);
3815
- console.log("[ScalarPlugin] Static analysis completed successfully.");
3816
- } catch (err) {
3817
- console.error("[ScalarPlugin] Failed to run static analysis:", err);
3818
- }
3819
- }
3820
- });
3821
- }
3822
- }
3823
- }
3824
4828
  function SecurityHeaders(options = {}) {
3825
4829
  const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
3826
4830
  const headers = {};
@@ -3944,18 +4948,18 @@ class MemoryStore extends events.EventEmitter {
3944
4948
  }
3945
4949
  set(sid, sess, cb) {
3946
4950
  this.sessions[sid] = JSON.stringify(sess);
3947
- cb && cb();
4951
+ cb?.();
3948
4952
  }
3949
4953
  destroy(sid, cb) {
3950
4954
  delete this.sessions[sid];
3951
- cb && cb();
4955
+ cb?.();
3952
4956
  }
3953
4957
  touch(sid, sess, cb) {
3954
4958
  const current = this.sessions[sid];
3955
4959
  if (current) {
3956
4960
  this.sessions[sid] = JSON.stringify(sess);
3957
4961
  }
3958
- cb && cb();
4962
+ cb?.();
3959
4963
  }
3960
4964
  all(cb) {
3961
4965
  const result = {};
@@ -3971,7 +4975,7 @@ class MemoryStore extends events.EventEmitter {
3971
4975
  }
3972
4976
  clear(cb) {
3973
4977
  this.sessions = {};
3974
- cb && cb();
4978
+ cb?.();
3975
4979
  }
3976
4980
  }
3977
4981
  function sign(val, secret) {
@@ -4149,29 +5153,51 @@ function Session(options) {
4149
5153
  return sessionMiddleware;
4150
5154
  }
4151
5155
  exports.$appRoot = $appRoot;
5156
+ exports.$bodyParseError = $bodyParseError;
5157
+ exports.$bodyParsed = $bodyParsed;
5158
+ exports.$bodyType = $bodyType;
5159
+ exports.$cachedBody = $cachedBody;
5160
+ exports.$cachedHost = $cachedHost;
5161
+ exports.$cachedHostname = $cachedHostname;
5162
+ exports.$cachedOrigin = $cachedOrigin;
5163
+ exports.$cachedProtocol = $cachedProtocol;
5164
+ exports.$cachedQuery = $cachedQuery;
4152
5165
  exports.$childControllers = $childControllers;
4153
5166
  exports.$childRouters = $childRouters;
4154
5167
  exports.$controllerPath = $controllerPath;
5168
+ exports.$debug = $debug;
4155
5169
  exports.$dispatch = $dispatch;
5170
+ exports.$eventMethods = $eventMethods;
5171
+ exports.$finalResponse = $finalResponse;
5172
+ exports.$io = $io;
4156
5173
  exports.$isApplication = $isApplication;
4157
5174
  exports.$isMounted = $isMounted;
4158
5175
  exports.$isRouter = $isRouter;
4159
5176
  exports.$middleware = $middleware;
4160
5177
  exports.$mountPath = $mountPath;
4161
5178
  exports.$parent = $parent;
5179
+ exports.$rawBody = $rawBody;
5180
+ exports.$requestId = $requestId;
4162
5181
  exports.$routeArgs = $routeArgs;
5182
+ exports.$routeMatched = $routeMatched;
4163
5183
  exports.$routeMethods = $routeMethods;
4164
5184
  exports.$routeSpec = $routeSpec;
4165
5185
  exports.$routes = $routes;
5186
+ exports.$socket = $socket;
5187
+ exports.$url = $url;
5188
+ exports.$ws = $ws;
4166
5189
  exports.All = All;
4167
5190
  exports.AuthPlugin = AuthPlugin;
4168
5191
  exports.Body = Body;
5192
+ exports.ClusterPlugin = ClusterPlugin;
4169
5193
  exports.Compression = Compression;
4170
5194
  exports.Container = Container;
4171
5195
  exports.Controller = Controller;
4172
5196
  exports.Cors = Cors;
4173
5197
  exports.Ctx = Ctx;
5198
+ exports.Dashboard = Dashboard;
4174
5199
  exports.Delete = Delete;
5200
+ exports.Event = Event;
4175
5201
  exports.Get = Get;
4176
5202
  exports.HTTPMethods = HTTPMethods;
4177
5203
  exports.Head = Head;
@@ -4189,16 +5215,13 @@ exports.RateLimit = RateLimit;
4189
5215
  exports.RateLimitMiddleware = RateLimitMiddleware;
4190
5216
  exports.Req = Req;
4191
5217
  exports.RouteParamType = RouteParamType;
4192
- exports.RouterRegistry = RouterRegistry;
4193
5218
  exports.ScalarPlugin = ScalarPlugin;
4194
5219
  exports.SecurityHeaders = SecurityHeaders;
4195
5220
  exports.Session = Session;
4196
5221
  exports.Shokupan = Shokupan;
4197
- exports.ShokupanApplicationTree = ShokupanApplicationTree;
4198
5222
  exports.ShokupanContext = ShokupanContext;
4199
5223
  exports.ShokupanRequest = ShokupanRequest;
4200
5224
  exports.ShokupanResponse = ShokupanResponse;
4201
- exports.ShokupanRouter = ShokupanRouter;
4202
5225
  exports.Spec = Spec;
4203
5226
  exports.Use = Use;
4204
5227
  exports.ValidationError = ValidationError;