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.js CHANGED
@@ -1,18 +1,122 @@
1
+ import { nanoid } from "nanoid";
1
2
  import { readFile } from "node:fs/promises";
3
+ import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { Surreal, RecordId } from "surrealdb";
2
6
  import { Eta } from "eta";
3
7
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
4
8
  import { resolve, join, sep, basename } from "path";
5
- import { AsyncLocalStorage } from "node:async_hooks";
6
- import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
7
9
  import * as os from "node:os";
10
+ import os__default from "node:os";
8
11
  import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
9
12
  import * as jose from "jose";
13
+ import cluster from "node:cluster";
14
+ import net from "node:net";
15
+ import { dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { monitorEventLoopDelay } from "node:perf_hooks";
18
+ import { OpenAPIAnalyzer } from "./analyzer-Ce_7JxZh.js";
10
19
  import * as zlib from "node:zlib";
11
20
  import Ajv from "ajv";
12
21
  import addFormats from "ajv-formats";
13
- import { OpenAPIAnalyzer } from "./openapi-analyzer-Ce_7JxZh.js";
14
22
  import { randomUUID, createHmac } from "crypto";
15
23
  import { EventEmitter } from "events";
24
+ const HTTP_STATUS = {
25
+ // 2xx Success
26
+ OK: 200,
27
+ CREATED: 201,
28
+ ACCEPTED: 202,
29
+ NO_CONTENT: 204,
30
+ // 3xx Redirection
31
+ MOVED_PERMANENTLY: 301,
32
+ FOUND: 302,
33
+ SEE_OTHER: 303,
34
+ NOT_MODIFIED: 304,
35
+ TEMPORARY_REDIRECT: 307,
36
+ PERMANENT_REDIRECT: 308,
37
+ // 4xx Client Errors
38
+ BAD_REQUEST: 400,
39
+ UNAUTHORIZED: 401,
40
+ FORBIDDEN: 403,
41
+ NOT_FOUND: 404,
42
+ METHOD_NOT_ALLOWED: 405,
43
+ REQUEST_TIMEOUT: 408,
44
+ CONFLICT: 409,
45
+ UNPROCESSABLE_ENTITY: 422,
46
+ TOO_MANY_REQUESTS: 429,
47
+ // 5xx Server Errors
48
+ INTERNAL_SERVER_ERROR: 500,
49
+ NOT_IMPLEMENTED: 501,
50
+ BAD_GATEWAY: 502,
51
+ SERVICE_UNAVAILABLE: 503,
52
+ GATEWAY_TIMEOUT: 504
53
+ };
54
+ const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
55
+ 100,
56
+ 101,
57
+ 102,
58
+ 103,
59
+ 200,
60
+ 201,
61
+ 202,
62
+ 203,
63
+ 204,
64
+ 205,
65
+ 206,
66
+ 207,
67
+ 208,
68
+ 226,
69
+ 300,
70
+ 301,
71
+ 302,
72
+ 303,
73
+ 304,
74
+ 305,
75
+ 306,
76
+ 307,
77
+ 308,
78
+ 400,
79
+ 401,
80
+ 402,
81
+ 403,
82
+ 404,
83
+ 405,
84
+ 406,
85
+ 407,
86
+ 408,
87
+ 409,
88
+ 410,
89
+ 411,
90
+ 412,
91
+ 413,
92
+ 414,
93
+ 415,
94
+ 416,
95
+ 417,
96
+ 418,
97
+ 421,
98
+ 422,
99
+ 423,
100
+ 424,
101
+ 425,
102
+ 426,
103
+ 428,
104
+ 429,
105
+ 431,
106
+ 451,
107
+ 500,
108
+ 501,
109
+ 502,
110
+ 503,
111
+ 504,
112
+ 505,
113
+ 506,
114
+ 507,
115
+ 508,
116
+ 510,
117
+ 511
118
+ ]);
119
+ const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
16
120
  class ShokupanResponse {
17
121
  _headers = null;
18
122
  _status = 200;
@@ -76,6 +180,40 @@ class ShokupanResponse {
76
180
  return this._headers !== null;
77
181
  }
78
182
  }
183
+ const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
184
+ const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
185
+ const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
186
+ const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
187
+ const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
188
+ const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
189
+ const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
190
+ const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
191
+ const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
192
+ const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
193
+ const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
194
+ const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
195
+ const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
196
+ const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
197
+ const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
198
+ const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
199
+ const $url = /* @__PURE__ */ Symbol.for("Shokupan.ctx.url");
200
+ const $requestId = /* @__PURE__ */ Symbol.for("Shokupan.ctx.requestId");
201
+ const $debug = /* @__PURE__ */ Symbol.for("Shokupan.ctx.debug");
202
+ const $finalResponse = /* @__PURE__ */ Symbol.for("Shokupan.ctx.finalResponse");
203
+ const $rawBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.rawBody");
204
+ const $cachedBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedBody");
205
+ const $bodyType = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyType");
206
+ const $bodyParsed = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParsed");
207
+ const $bodyParseError = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParseError");
208
+ const $routeMatched = /* @__PURE__ */ Symbol.for("Shokupan.ctx.routeMatched");
209
+ const $cachedHostname = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHostname");
210
+ const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol");
211
+ const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
212
+ const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
213
+ const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
214
+ const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
215
+ const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
216
+ const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
79
217
  function isValidCookieDomain(domain, currentHost) {
80
218
  const hostWithoutPort = currentHost.split(":")[0];
81
219
  if (domain === hostWithoutPort) return true;
@@ -85,72 +223,6 @@ function isValidCookieDomain(domain, currentHost) {
85
223
  }
86
224
  return false;
87
225
  }
88
- const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
89
- 100,
90
- 101,
91
- 102,
92
- 103,
93
- 200,
94
- 201,
95
- 202,
96
- 203,
97
- 204,
98
- 205,
99
- 206,
100
- 207,
101
- 208,
102
- 226,
103
- 300,
104
- 301,
105
- 302,
106
- 303,
107
- 304,
108
- 305,
109
- 306,
110
- 307,
111
- 308,
112
- 400,
113
- 401,
114
- 402,
115
- 403,
116
- 404,
117
- 405,
118
- 406,
119
- 407,
120
- 408,
121
- 409,
122
- 410,
123
- 411,
124
- 412,
125
- 413,
126
- 414,
127
- 415,
128
- 416,
129
- 417,
130
- 418,
131
- 421,
132
- 422,
133
- 423,
134
- 424,
135
- 425,
136
- 426,
137
- 428,
138
- 429,
139
- 431,
140
- 451,
141
- 500,
142
- 501,
143
- 502,
144
- 503,
145
- 504,
146
- 505,
147
- 506,
148
- 507,
149
- 508,
150
- 510,
151
- 511
152
- ]);
153
- const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
154
226
  class ShokupanContext {
155
227
  constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
156
228
  this.request = request;
@@ -179,28 +251,43 @@ class ShokupanContext {
179
251
  state;
180
252
  handlerStack = [];
181
253
  response;
182
- _debug;
183
- _finalResponse;
184
- _rawBody;
254
+ [$debug];
255
+ [$finalResponse];
256
+ [$rawBody];
185
257
  // Raw body for compression optimization
186
258
  // Body caching to avoid double parsing
187
- _url;
188
- _cachedBody;
189
- _bodyType;
190
- _bodyParsed = false;
191
- _bodyParseError;
259
+ [$url];
260
+ [$cachedBody];
261
+ [$bodyType];
262
+ [$bodyParsed] = false;
263
+ [$bodyParseError];
264
+ [$routeMatched] = false;
192
265
  // Cached URL properties to avoid repeated parsing
193
- _cachedHostname;
194
- _cachedProtocol;
195
- _cachedHost;
196
- _cachedOrigin;
197
- _cachedQuery;
266
+ [$cachedHostname];
267
+ [$cachedProtocol];
268
+ [$cachedHost];
269
+ [$cachedOrigin];
270
+ [$cachedQuery];
271
+ [$ws];
272
+ [$socket];
273
+ [$io];
274
+ /**
275
+ * JSX Rendering Function
276
+ */
277
+ renderer;
278
+ setRenderer(renderer) {
279
+ this.renderer = renderer;
280
+ }
281
+ [$requestId];
282
+ get requestId() {
283
+ return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid();
284
+ }
198
285
  get url() {
199
- if (!this._url) {
286
+ if (!this[$url]) {
200
287
  const urlString = this.request.url || "http://localhost/";
201
- this._url = new URL(urlString);
288
+ this[$url] = new URL(urlString);
202
289
  }
203
- return this._url;
290
+ return this[$url];
204
291
  }
205
292
  /**
206
293
  * Base request
@@ -218,7 +305,7 @@ class ShokupanContext {
218
305
  * Request path
219
306
  */
220
307
  get path() {
221
- if (this._url) return this._url.pathname;
308
+ if (this[$url]) return this[$url].pathname;
222
309
  const url = this.request.url;
223
310
  let queryIndex = url.indexOf("?");
224
311
  const end = queryIndex === -1 ? url.length : queryIndex;
@@ -243,7 +330,7 @@ class ShokupanContext {
243
330
  * Request query params
244
331
  */
245
332
  get query() {
246
- if (this._cachedQuery) return this._cachedQuery;
333
+ if (this[$cachedQuery]) return this[$cachedQuery];
247
334
  const q = /* @__PURE__ */ Object.create(null);
248
335
  const blocklist = ["__proto__", "constructor", "prototype"];
249
336
  const entries = Object.entries(this.url.searchParams);
@@ -260,7 +347,7 @@ class ShokupanContext {
260
347
  q[key] = value;
261
348
  }
262
349
  }
263
- this._cachedQuery = q;
350
+ this[$cachedQuery] = q;
264
351
  return q;
265
352
  }
266
353
  /**
@@ -273,19 +360,19 @@ class ShokupanContext {
273
360
  * Request hostname (e.g. "localhost")
274
361
  */
275
362
  get hostname() {
276
- return this._cachedHostname ??= this.url.hostname;
363
+ return this[$cachedHostname] ??= this.url.hostname;
277
364
  }
278
365
  /**
279
366
  * Request host (e.g. "localhost:3000")
280
367
  */
281
368
  get host() {
282
- return this._cachedHost ??= this.url.host;
369
+ return this[$cachedHost] ??= this.url.host;
283
370
  }
284
371
  /**
285
372
  * Request protocol (e.g. "http:", "https:")
286
373
  */
287
374
  get protocol() {
288
- return this._cachedProtocol ??= this.url.protocol;
375
+ return this[$cachedProtocol] ??= this.url.protocol;
289
376
  }
290
377
  /**
291
378
  * Whether request is secure (https)
@@ -297,7 +384,7 @@ class ShokupanContext {
297
384
  * Request origin (e.g. "http://localhost:3000")
298
385
  */
299
386
  get origin() {
300
- return this._cachedOrigin ??= this.url.origin;
387
+ return this[$cachedOrigin] ??= this.url.origin;
301
388
  }
302
389
  /**
303
390
  * Request headers
@@ -318,6 +405,24 @@ class ShokupanContext {
318
405
  get res() {
319
406
  return this.response;
320
407
  }
408
+ /**
409
+ * Raw WebSocket connection
410
+ */
411
+ get ws() {
412
+ return this[$ws];
413
+ }
414
+ /**
415
+ * Socket.io socket
416
+ */
417
+ get socket() {
418
+ return this[$socket];
419
+ }
420
+ /**
421
+ * Socket.io server
422
+ */
423
+ get io() {
424
+ return this[$io];
425
+ }
321
426
  /**
322
427
  * Helper to set a header on the response
323
428
  * @param key Header key
@@ -327,6 +432,20 @@ class ShokupanContext {
327
432
  this.response.set(key, value);
328
433
  return this;
329
434
  }
435
+ isUpgraded = false;
436
+ /**
437
+ * Upgrades the request to a WebSocket connection.
438
+ * @param options Upgrade options
439
+ * @returns true if upgraded, false otherwise
440
+ */
441
+ upgrade(options) {
442
+ if (!this.server) return false;
443
+ const success = this.server.upgrade(this.req, options);
444
+ if (success) {
445
+ this.isUpgraded = true;
446
+ }
447
+ return success;
448
+ }
330
449
  /**
331
450
  * Set a cookie
332
451
  * @param name Cookie name
@@ -404,33 +523,37 @@ class ShokupanContext {
404
523
  * The body is only parsed once and cached for subsequent reads.
405
524
  */
406
525
  async body() {
407
- if (this._bodyParseError) {
408
- throw this._bodyParseError;
526
+ if (this[$bodyParseError]) {
527
+ throw this[$bodyParseError];
409
528
  }
410
- if (this._bodyParsed) {
411
- return this._cachedBody;
529
+ if (this[$bodyParsed]) {
530
+ return this[$cachedBody];
412
531
  }
413
532
  const contentType = this.request.headers.get("content-type") || "";
414
533
  if (contentType.includes("application/json") || contentType.includes("+json")) {
415
- const rawText = await this.readRawBody();
416
534
  const parserType = this.app?.applicationConfig?.jsonParser || "native";
417
535
  if (parserType === "native") {
418
- this._cachedBody = JSON.parse(rawText);
536
+ try {
537
+ this[$cachedBody] = await this.request.json();
538
+ } catch (e) {
539
+ throw e;
540
+ }
419
541
  } else {
542
+ const rawText = await this.request.text();
420
543
  const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
421
544
  const parser = getJSONParser(parserType);
422
- this._cachedBody = parser(rawText);
545
+ this[$cachedBody] = parser(rawText);
423
546
  }
424
- this._bodyType = "json";
547
+ this[$bodyType] = "json";
425
548
  } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
426
- this._cachedBody = await this.request.formData();
427
- this._bodyType = "formData";
549
+ this[$cachedBody] = await this.request.formData();
550
+ this[$bodyType] = "formData";
428
551
  } else {
429
- this._cachedBody = await this.readRawBody();
430
- this._bodyType = "text";
552
+ this[$cachedBody] = await this.request.text();
553
+ this[$bodyType] = "text";
431
554
  }
432
- this._bodyParsed = true;
433
- return this._cachedBody;
555
+ this[$bodyParsed] = true;
556
+ return this[$cachedBody];
434
557
  }
435
558
  /**
436
559
  * Pre-parse the request body before handler execution.
@@ -438,7 +561,7 @@ class ShokupanContext {
438
561
  * Errors are deferred until the body is actually accessed in the handler.
439
562
  */
440
563
  async parseBody() {
441
- if (this._bodyParsed) {
564
+ if (this[$bodyParsed]) {
442
565
  return;
443
566
  }
444
567
  if (this.request.method === "GET" || this.request.method === "HEAD") {
@@ -447,7 +570,7 @@ class ShokupanContext {
447
570
  try {
448
571
  await this.body();
449
572
  } catch (error) {
450
- this._bodyParseError = error;
573
+ this[$bodyParseError] = error;
451
574
  }
452
575
  }
453
576
  /**
@@ -497,10 +620,21 @@ class ShokupanContext {
497
620
  throw new Error(`Invalid HTTP status code: ${status}`);
498
621
  }
499
622
  if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
500
- this._rawBody = body;
623
+ this[$rawBody] = body;
624
+ }
625
+ return this[$finalResponse] ??= new Response(body, { status, headers });
626
+ }
627
+ /**
628
+ * Emit an event to the client (WebSocket only)
629
+ * @param event Event name
630
+ * @param data Event data (Must be JSON serializable)
631
+ */
632
+ emit(event, data) {
633
+ if (this[$ws]) {
634
+ this[$ws].send(JSON.stringify({ event, data }));
635
+ } else if (this[$socket]) {
636
+ this[$socket].emit(event, data);
501
637
  }
502
- this._finalResponse = new Response(body, { status, headers });
503
- return this._finalResponse;
504
638
  }
505
639
  /**
506
640
  * Respond with a JSON object
@@ -511,18 +645,18 @@ class ShokupanContext {
511
645
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
512
646
  }
513
647
  const jsonString = JSON.stringify(data);
514
- this._rawBody = jsonString;
648
+ this[$rawBody] = jsonString;
515
649
  if (!headers && !this.response.hasPopulatedHeaders) {
516
- this._finalResponse = new Response(jsonString, {
650
+ this[$finalResponse] = new Response(jsonString, {
517
651
  status: finalStatus,
518
652
  headers: { "content-type": "application/json" }
519
653
  });
520
- return this._finalResponse;
654
+ return this[$finalResponse];
521
655
  }
522
656
  const finalHeaders = this.mergeHeaders(headers);
523
657
  finalHeaders.set("content-type", "application/json");
524
- this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
525
- return this._finalResponse;
658
+ this[$finalResponse] = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
659
+ return this[$finalResponse];
526
660
  }
527
661
  /**
528
662
  * Respond with a text string
@@ -532,18 +666,18 @@ class ShokupanContext {
532
666
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
533
667
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
534
668
  }
535
- this._rawBody = data;
669
+ this[$rawBody] = data;
536
670
  if (!headers && !this.response.hasPopulatedHeaders) {
537
- this._finalResponse = new Response(data, {
671
+ this[$finalResponse] = new Response(data, {
538
672
  status: finalStatus,
539
673
  headers: { "content-type": "text/plain; charset=utf-8" }
540
674
  });
541
- return this._finalResponse;
675
+ return this[$finalResponse];
542
676
  }
543
677
  const finalHeaders = this.mergeHeaders(headers);
544
678
  finalHeaders.set("content-type", "text/plain; charset=utf-8");
545
- this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
546
- return this._finalResponse;
679
+ this[$finalResponse] = new Response(data, { status: finalStatus, headers: finalHeaders });
680
+ return this[$finalResponse];
547
681
  }
548
682
  /**
549
683
  * Respond with HTML content
@@ -555,9 +689,9 @@ class ShokupanContext {
555
689
  }
556
690
  const finalHeaders = this.mergeHeaders(headers);
557
691
  finalHeaders.set("content-type", "text/html; charset=utf-8");
558
- this._rawBody = html;
559
- this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
560
- return this._finalResponse;
692
+ this[$rawBody] = html;
693
+ this[$finalResponse] = new Response(html, { status: finalStatus, headers: finalHeaders });
694
+ return this[$finalResponse];
561
695
  }
562
696
  /**
563
697
  * Respond with a redirect
@@ -568,8 +702,8 @@ class ShokupanContext {
568
702
  }
569
703
  const headers = this.mergeHeaders();
570
704
  headers.set("Location", url);
571
- this._finalResponse = new Response(null, { status, headers });
572
- return this._finalResponse;
705
+ this[$finalResponse] = new Response(null, { status, headers });
706
+ return this[$finalResponse];
573
707
  }
574
708
  /**
575
709
  * Respond with a status code
@@ -580,8 +714,8 @@ class ShokupanContext {
580
714
  throw new Error(`Invalid HTTP status code: ${status}`);
581
715
  }
582
716
  const headers = this.mergeHeaders();
583
- this._finalResponse = new Response(null, { status, headers });
584
- return this._finalResponse;
717
+ this[$finalResponse] = new Response(null, { status, headers });
718
+ return this[$finalResponse];
585
719
  }
586
720
  /**
587
721
  * Respond with a file
@@ -593,21 +727,17 @@ class ShokupanContext {
593
727
  throw new Error(`Invalid HTTP status code: ${status}`);
594
728
  }
595
729
  if (typeof Bun !== "undefined") {
596
- this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
597
- return this._finalResponse;
730
+ this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers });
731
+ return this[$finalResponse];
598
732
  } else {
599
733
  const fileBuffer = await readFile(path);
600
734
  if (fileOptions?.type) {
601
735
  headers.set("content-type", fileOptions.type);
602
736
  }
603
- this._finalResponse = new Response(fileBuffer, { status, headers });
604
- return this._finalResponse;
737
+ this[$finalResponse] = new Response(fileBuffer, { status, headers });
738
+ return this[$finalResponse];
605
739
  }
606
740
  }
607
- /**
608
- * JSX Rendering Function
609
- */
610
- renderer;
611
741
  /**
612
742
  * Render a JSX element
613
743
  * @param element JSX Element
@@ -626,284 +756,67 @@ class ShokupanContext {
626
756
  return this.html(html, status, headers);
627
757
  }
628
758
  }
629
- function RateLimitMiddleware(options = {}) {
630
- const windowMs = options.windowMs || 60 * 1e3;
631
- const max = options.limit || options.max || 5;
632
- const message = options.message || "Too many requests, please try again later.";
633
- const statusCode = options.statusCode || 429;
634
- const headers = options.headers !== false;
635
- const mode = options.mode || "user";
636
- const trustedProxies = options.trustedProxies || [];
637
- const keyGenerator = options.keyGenerator || ((ctx) => {
638
- if (mode === "absolute") {
639
- return "global";
640
- }
641
- const xForwardedFor = ctx.headers.get("x-forwarded-for");
642
- if (xForwardedFor && trustedProxies.length > 0) {
643
- const ips = xForwardedFor.split(",").map((ip) => ip.trim());
644
- for (let i = ips.length - 1; i >= 0; i--) {
645
- const ip = ips[i];
646
- if (!trustedProxies.includes(ip)) {
647
- if (/^[\d.:a-fA-F]+$/.test(ip)) {
648
- return ip;
649
- }
650
- }
759
+ const compose = (middleware) => {
760
+ if (!middleware.length) {
761
+ return (context2, next) => {
762
+ return next ? next() : Promise.resolve();
763
+ };
764
+ }
765
+ return function dispatch(context2, next) {
766
+ let index = -1;
767
+ async function runner(i) {
768
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
769
+ index = i;
770
+ if (i >= middleware.length) {
771
+ return next ? next() : Promise.resolve();
651
772
  }
652
- }
653
- return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
654
- });
655
- const skip = options.skip || (() => false);
656
- const hits = /* @__PURE__ */ new Map();
657
- const interval = setInterval(() => {
658
- const now = Date.now();
659
- const entries = Array.from(hits.entries());
660
- for (let i = 0; i < entries.length; i++) {
661
- const [key, record] = entries[i];
662
- if (record.resetTime <= now) {
663
- hits.delete(key);
773
+ const fn = middleware[i];
774
+ if (!context2[$debug]) {
775
+ return fn(context2, () => runner(i + 1));
776
+ }
777
+ const debug = context2[$debug];
778
+ const debugId = fn._debugId || fn.name || "anonymous";
779
+ const previousNode = debug.getCurrentNode();
780
+ debug.trackEdge(previousNode, debugId);
781
+ debug.setNode(debugId);
782
+ const start = performance.now();
783
+ try {
784
+ const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
785
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
786
+ return res;
787
+ } catch (err) {
788
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
789
+ return Promise.reject(err);
790
+ } finally {
791
+ if (previousNode) debug.setNode(previousNode);
664
792
  }
665
793
  }
666
- }, windowMs);
667
- if (interval.unref) interval.unref();
668
- const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
669
- if (skip(ctx)) return next();
670
- const key = keyGenerator(ctx);
671
- const now = Date.now();
672
- let record = hits.get(key);
673
- if (!record || record.resetTime <= now) {
674
- record = {
675
- hits: 0,
676
- resetTime: now + windowMs
677
- };
678
- hits.set(key, record);
679
- }
680
- record.hits++;
681
- const remaining = Math.max(0, max - record.hits);
682
- const resetTime = Math.ceil(record.resetTime / 1e3);
683
- const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
684
- const setHeaders = (res) => {
685
- if (!headers || !res || !res.headers) return;
686
- try {
687
- res.headers.set("X-RateLimit-Limit", String(max));
688
- res.headers.set("X-RateLimit-Remaining", String(remaining));
689
- res.headers.set("X-RateLimit-Reset", String(resetTime));
690
- } catch (e) {
691
- }
692
- };
693
- if (record.hits > max) {
694
- typeof message === "object" ? JSON.stringify(message) : String(message);
695
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
696
- if (headers) {
697
- setHeaders(res);
698
- res.headers.set("Retry-After", String(retryAfter));
699
- }
700
- return res;
701
- }
702
- const response = await next();
703
- if (response instanceof Response && headers) {
704
- setHeaders(response);
705
- }
706
- return response;
707
- };
708
- rateLimitMiddleware.isBuiltin = true;
709
- rateLimitMiddleware.pluginName = "RateLimit";
710
- return rateLimitMiddleware;
711
- }
712
- const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
713
- const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
714
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
715
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
716
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
717
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
718
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
719
- const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
720
- const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
721
- const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
722
- const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
723
- const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
724
- const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
725
- const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
726
- const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
727
- const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
728
- var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
729
- RouteParamType2["BODY"] = "BODY";
730
- RouteParamType2["PARAM"] = "PARAM";
731
- RouteParamType2["QUERY"] = "QUERY";
732
- RouteParamType2["HEADER"] = "HEADER";
733
- RouteParamType2["REQUEST"] = "REQUEST";
734
- RouteParamType2["CONTEXT"] = "CONTEXT";
735
- return RouteParamType2;
736
- })(RouteParamType || {});
737
- function Controller(path = "/") {
738
- return (target) => {
739
- target[$controllerPath] = path;
740
- };
741
- }
742
- function Use(...middleware) {
743
- return (target, propertyKey, descriptor) => {
744
- if (!propertyKey) {
745
- const existing = target[$middleware] || [];
746
- target[$middleware] = [...existing, ...middleware];
747
- } else {
748
- if (!target[$middleware]) {
749
- target[$middleware] = /* @__PURE__ */ new Map();
750
- }
751
- const existing = target[$middleware].get(propertyKey) || [];
752
- target[$middleware].set(propertyKey, [...existing, ...middleware]);
753
- }
754
- };
755
- }
756
- function createParamDecorator(type) {
757
- return (name) => {
758
- return (target, propertyKey, parameterIndex) => {
759
- if (!target[$routeArgs]) {
760
- target[$routeArgs] = /* @__PURE__ */ new Map();
761
- }
762
- if (!target[$routeArgs].has(propertyKey)) {
763
- target[$routeArgs].set(propertyKey, []);
764
- }
765
- target[$routeArgs].get(propertyKey).push({
766
- index: parameterIndex,
767
- type,
768
- name
769
- });
770
- };
771
- };
772
- }
773
- const Body = createParamDecorator(RouteParamType.BODY);
774
- const Param = createParamDecorator(RouteParamType.PARAM);
775
- const Query = createParamDecorator(RouteParamType.QUERY);
776
- const Headers$1 = createParamDecorator(RouteParamType.HEADER);
777
- const Req = createParamDecorator(RouteParamType.REQUEST);
778
- const Ctx = createParamDecorator(RouteParamType.CONTEXT);
779
- function Spec(spec) {
780
- return (target, propertyKey, descriptor) => {
781
- if (!target[$routeSpec]) {
782
- target[$routeSpec] = /* @__PURE__ */ new Map();
783
- }
784
- target[$routeSpec].set(propertyKey, spec);
785
- };
786
- }
787
- function createMethodDecorator(method) {
788
- return (path = "/") => {
789
- return (target, propertyKey, descriptor) => {
790
- if (!target[$routeMethods]) {
791
- target[$routeMethods] = /* @__PURE__ */ new Map();
792
- }
793
- target[$routeMethods].set(propertyKey, {
794
- method,
795
- path
796
- });
797
- };
798
- };
799
- }
800
- const Get = createMethodDecorator("GET");
801
- const Post = createMethodDecorator("POST");
802
- const Put = createMethodDecorator("PUT");
803
- const Delete = createMethodDecorator("DELETE");
804
- const Patch = createMethodDecorator("PATCH");
805
- const Options = createMethodDecorator("OPTIONS");
806
- const Head = createMethodDecorator("HEAD");
807
- const All = createMethodDecorator("ALL");
808
- function RateLimit(options) {
809
- return Use(RateLimitMiddleware(options));
810
- }
811
- class Container {
812
- static services = /* @__PURE__ */ new Map();
813
- static register(target, instance) {
814
- this.services.set(target, instance);
815
- }
816
- static get(target) {
817
- return this.services.get(target);
818
- }
819
- static has(target) {
820
- return this.services.has(target);
821
- }
822
- static resolve(target) {
823
- if (this.services.has(target)) {
824
- return this.services.get(target);
825
- }
826
- const instance = new target();
827
- this.services.set(target, instance);
828
- return instance;
829
- }
830
- }
831
- function Injectable() {
832
- return (target) => {
833
- };
834
- }
835
- function Inject(token) {
836
- return (target, key) => {
837
- Object.defineProperty(target, key, {
838
- get: () => Container.resolve(token),
839
- enumerable: true,
840
- configurable: true
841
- });
794
+ return runner(0);
842
795
  };
843
- }
844
- const compose = (middleware) => {
845
- if (!middleware.length) {
846
- return (context2, next) => {
847
- return next ? next() : Promise.resolve();
848
- };
849
- }
850
- return function dispatch(context2, next) {
851
- let index = -1;
852
- async function runner(i) {
853
- if (i <= index) return Promise.reject(new Error("next() called multiple times"));
854
- index = i;
855
- if (i >= middleware.length) {
856
- return next ? next() : Promise.resolve();
857
- }
858
- const fn = middleware[i];
859
- if (!context2._debug) {
860
- return fn(context2, () => runner(i + 1));
796
+ };
797
+ const tracer = trace.getTracer("shokupan.middleware");
798
+ function traceHandler(fn, name) {
799
+ return async function(...args) {
800
+ return tracer.startActiveSpan(`route handler - ${name}`, {
801
+ kind: SpanKind.INTERNAL,
802
+ attributes: {
803
+ "http.route": name,
804
+ "component": "shokupan.route"
861
805
  }
862
- const debug = context2._debug;
863
- const debugId = fn._debugId || fn.name || "anonymous";
864
- const previousNode = debug.getCurrentNode();
865
- debug.trackEdge(previousNode, debugId);
866
- debug.setNode(debugId);
867
- const start = performance.now();
806
+ }, async (span) => {
868
807
  try {
869
- const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
870
- debug.trackStep(debugId, "middleware", performance.now() - start, "success");
871
- return res;
808
+ const result = await fn.apply(this, args);
809
+ return result;
872
810
  } catch (err) {
873
- debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
874
- return Promise.reject(err);
811
+ span.recordException(err);
812
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
813
+ throw err;
875
814
  } finally {
876
- if (previousNode) debug.setNode(previousNode);
815
+ span.end();
877
816
  }
878
- }
879
- return runner(0);
817
+ });
880
818
  };
881
- };
882
- class ShokupanRequestBase {
883
- method;
884
- url;
885
- headers;
886
- body;
887
- async json() {
888
- return JSON.parse(this.body);
889
- }
890
- async text() {
891
- return this.body;
892
- }
893
- async formData() {
894
- if (this.body instanceof FormData) {
895
- return this.body;
896
- }
897
- return new Response(this.body, { headers: this.headers }).formData();
898
- }
899
- constructor(props) {
900
- Object.assign(this, props);
901
- if (!(this.headers instanceof Headers)) {
902
- this.headers = new Headers(this.headers);
903
- }
904
- }
905
819
  }
906
- const ShokupanRequest = ShokupanRequestBase;
907
820
  function isObject(item) {
908
821
  return item && typeof item === "object" && !Array.isArray(item);
909
822
  }
@@ -1129,7 +1042,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1129
1042
  const defaultTagName = options.defaultTag || "Application";
1130
1043
  let astRoutes = [];
1131
1044
  try {
1132
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-Ce_7JxZh.js");
1045
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-Ce_7JxZh.js");
1133
1046
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
1134
1047
  const { applications } = await analyzer.analyze();
1135
1048
  astRoutes = await getAstRoutes(applications);
@@ -1318,6 +1231,40 @@ async function generateOpenApi(rootRouter, options = {}) {
1318
1231
  "x-tagGroups": xTagGroups
1319
1232
  };
1320
1233
  }
1234
+ class RequestContextStore {
1235
+ request;
1236
+ span;
1237
+ }
1238
+ const asyncContext = new AsyncLocalStorage();
1239
+ class HttpError extends Error {
1240
+ status;
1241
+ constructor(message, status) {
1242
+ super(message);
1243
+ this.name = "HttpError";
1244
+ this.status = status;
1245
+ if (Error.captureStackTrace) {
1246
+ Error.captureStackTrace(this, HttpError);
1247
+ }
1248
+ }
1249
+ }
1250
+ function getErrorStatus(err) {
1251
+ if (!err || typeof err !== "object") {
1252
+ return 500;
1253
+ }
1254
+ if (typeof err.status === "number") {
1255
+ return err.status;
1256
+ }
1257
+ if (typeof err.statusCode === "number") {
1258
+ return err.statusCode;
1259
+ }
1260
+ return 500;
1261
+ }
1262
+ class EventError extends HttpError {
1263
+ constructor(message = "Event Error") {
1264
+ super(message, 500);
1265
+ this.name = "EventError";
1266
+ }
1267
+ }
1321
1268
  const eta$1 = new Eta();
1322
1269
  function serveStatic(config, prefix) {
1323
1270
  const rootPath = resolve(config.root || ".");
@@ -1470,14 +1417,162 @@ function serveStatic(config, prefix) {
1470
1417
  serveStaticMiddleware.pluginName = "ServeStatic";
1471
1418
  return serveStaticMiddleware;
1472
1419
  }
1473
- class RouterTrie {
1474
- root;
1475
- constructor() {
1476
- this.root = this.createNode();
1477
- }
1478
- createNode() {
1479
- return {
1480
- children: {}
1420
+ const G = globalThis;
1421
+ G.__shokupan_db = G.__shokupan_db || null;
1422
+ G.__shokupan_db_promise = G.__shokupan_db_promise || null;
1423
+ async function ensureDb() {
1424
+ if (G.__shokupan_db) return G.__shokupan_db;
1425
+ if (G.__shokupan_db_promise) return G.__shokupan_db_promise;
1426
+ G.__shokupan_db_promise = (async () => {
1427
+ try {
1428
+ const { createNodeEngines } = await import("@surrealdb/node");
1429
+ const surreal = await import("surrealdb");
1430
+ const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1431
+ const _db = new Surreal({
1432
+ engines: createNodeEngines()
1433
+ });
1434
+ await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1435
+ await _db.query(`
1436
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1437
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1438
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1439
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1440
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1441
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1442
+ DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
1443
+ `);
1444
+ G.__shokupan_db = _db;
1445
+ return _db;
1446
+ } catch (e) {
1447
+ G.__shokupan_db_promise = null;
1448
+ if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1449
+ throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1450
+ }
1451
+ throw e;
1452
+ }
1453
+ })();
1454
+ return G.__shokupan_db_promise;
1455
+ }
1456
+ const datastore = {
1457
+ async get(recordId) {
1458
+ await ensureDb();
1459
+ return G.__shokupan_db.select(recordId);
1460
+ },
1461
+ async set(recordId, value) {
1462
+ await ensureDb();
1463
+ return G.__shokupan_db.upsert(recordId).content(value);
1464
+ },
1465
+ async query(query, vars) {
1466
+ await ensureDb();
1467
+ try {
1468
+ return G.__shokupan_db.query(query, vars).collect();
1469
+ } catch (e) {
1470
+ console.error("DS ERROR:", e);
1471
+ throw e;
1472
+ }
1473
+ },
1474
+ get ready() {
1475
+ return ensureDb().then(() => void 0);
1476
+ }
1477
+ };
1478
+ process.on("exit", async () => {
1479
+ if (G.__shokupan_db) await G.__shokupan_db.close();
1480
+ });
1481
+ class Container {
1482
+ static services = /* @__PURE__ */ new Map();
1483
+ static register(target, instance) {
1484
+ this.services.set(target, instance);
1485
+ }
1486
+ static get(target) {
1487
+ return this.services.get(target);
1488
+ }
1489
+ static has(target) {
1490
+ return this.services.has(target);
1491
+ }
1492
+ static resolve(target) {
1493
+ if (this.services.has(target)) {
1494
+ return this.services.get(target);
1495
+ }
1496
+ const instance = new target();
1497
+ this.services.set(target, instance);
1498
+ return instance;
1499
+ }
1500
+ }
1501
+ function Injectable() {
1502
+ return (target) => {
1503
+ };
1504
+ }
1505
+ function Inject(token) {
1506
+ return (target, key) => {
1507
+ Object.defineProperty(target, key, {
1508
+ get: () => Container.resolve(token),
1509
+ enumerable: true,
1510
+ configurable: true
1511
+ });
1512
+ };
1513
+ }
1514
+ class ShokupanRequestBase {
1515
+ method;
1516
+ url;
1517
+ headers;
1518
+ body;
1519
+ async json() {
1520
+ return JSON.parse(this.body);
1521
+ }
1522
+ async text() {
1523
+ return this.body;
1524
+ }
1525
+ async formData() {
1526
+ if (this.body instanceof FormData) {
1527
+ return this.body;
1528
+ }
1529
+ return new Response(this.body, { headers: this.headers }).formData();
1530
+ }
1531
+ constructor(props) {
1532
+ Object.assign(this, props);
1533
+ if (!(this.headers instanceof Headers)) {
1534
+ this.headers = new Headers(this.headers);
1535
+ }
1536
+ }
1537
+ }
1538
+ const ShokupanRequest = ShokupanRequestBase;
1539
+ function getCallerInfo(skipFrames = 1) {
1540
+ let file = "unknown";
1541
+ let line = 0;
1542
+ try {
1543
+ const err = new Error();
1544
+ const stack = err.stack?.split("\n") || [];
1545
+ let found = 0;
1546
+ for (let i = 1; i < stack.length; i++) {
1547
+ const l = stack[i];
1548
+ if (!l.includes(":")) continue;
1549
+ if (l.includes("node_modules")) continue;
1550
+ if (l.includes("bun:main")) continue;
1551
+ if (l.includes("src/util/stack.ts")) continue;
1552
+ if (l.includes("src/router.ts")) continue;
1553
+ if (l.includes("src/shokupan.ts")) continue;
1554
+ found++;
1555
+ if (found >= skipFrames) {
1556
+ const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1557
+ if (match) {
1558
+ file = match[1];
1559
+ line = parseInt(match[2], 10);
1560
+ return { file, line };
1561
+ }
1562
+ }
1563
+ }
1564
+ } catch (e) {
1565
+ }
1566
+ return { file, line };
1567
+ }
1568
+ class RouterTrie {
1569
+ root;
1570
+ constructor() {
1571
+ this.root = this.createNode();
1572
+ }
1573
+ createNode() {
1574
+ return {
1575
+ children: {}
1481
1576
  };
1482
1577
  }
1483
1578
  insert(method, path, handler) {
@@ -1570,124 +1665,16 @@ class RouterTrie {
1570
1665
  return s.split("/");
1571
1666
  }
1572
1667
  }
1573
- const asyncContext = new AsyncLocalStorage();
1574
- let db;
1575
- let dbPromise = null;
1576
- let RecordId;
1577
- async function ensureDb() {
1578
- if (db) return db;
1579
- if (dbPromise) return dbPromise;
1580
- dbPromise = (async () => {
1581
- try {
1582
- const { createNodeEngines } = await import("@surrealdb/node");
1583
- const surreal = await import("surrealdb");
1584
- const Surreal = surreal.Surreal;
1585
- RecordId = surreal.RecordId;
1586
- const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1587
- const _db = new Surreal({
1588
- engines: createNodeEngines()
1589
- });
1590
- await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1591
- await _db.query(`
1592
- DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1593
- DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1594
- DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1595
- DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1596
- DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1597
- DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1598
- `);
1599
- db = _db;
1600
- return db;
1601
- } catch (e) {
1602
- dbPromise = null;
1603
- if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1604
- throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1605
- }
1606
- throw e;
1607
- }
1608
- })();
1609
- return dbPromise;
1610
- }
1611
- const datastore = {
1612
- async get(store, key) {
1613
- await ensureDb();
1614
- return db.select(new RecordId(store, key));
1615
- },
1616
- async set(store, key, value) {
1617
- await ensureDb();
1618
- return db.create(new RecordId(store, key)).content(value);
1619
- },
1620
- async query(query, vars) {
1621
- await ensureDb();
1622
- try {
1623
- const r = await db.query(query, vars);
1624
- return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
1625
- } catch (e) {
1626
- console.error("DS ERROR:", e);
1627
- throw e;
1628
- }
1629
- },
1630
- get ready() {
1631
- return ensureDb().then(() => void 0);
1632
- }
1633
- };
1634
- process.on("exit", async () => {
1635
- if (db) await db.close();
1636
- });
1637
- const tracer = trace.getTracer("shokupan.middleware");
1638
- function traceHandler(fn, name) {
1639
- return async function(...args) {
1640
- return tracer.startActiveSpan(`route handler - ${name}`, {
1641
- kind: SpanKind.INTERNAL,
1642
- attributes: {
1643
- "http.route": name,
1644
- "component": "shokupan.route"
1645
- }
1646
- }, async (span) => {
1647
- try {
1648
- const result = await fn.apply(this, args);
1649
- return result;
1650
- } catch (err) {
1651
- span.recordException(err);
1652
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
1653
- throw err;
1654
- } finally {
1655
- span.end();
1656
- }
1657
- });
1658
- };
1659
- }
1660
- function getCallerInfo(skipFrames = 1) {
1661
- let file = "unknown";
1662
- let line = 0;
1663
- try {
1664
- const err = new Error();
1665
- const stack = err.stack?.split("\n") || [];
1666
- let found = 0;
1667
- for (let i = 1; i < stack.length; i++) {
1668
- const l = stack[i];
1669
- if (!l.includes(":")) continue;
1670
- if (l.includes("node_modules")) continue;
1671
- if (l.includes("bun:main")) continue;
1672
- if (l.includes("src/util/stack.ts")) continue;
1673
- if (l.includes("src/router.ts")) continue;
1674
- if (l.includes("src/shokupan.ts")) continue;
1675
- found++;
1676
- if (found >= skipFrames) {
1677
- const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1678
- if (match) {
1679
- file = match[1];
1680
- line = parseInt(match[2], 10);
1681
- return { file, line };
1682
- }
1683
- }
1684
- }
1685
- } catch (e) {
1686
- }
1687
- return { file, line };
1688
- }
1689
- const RouterRegistry = /* @__PURE__ */ new Map();
1690
- const ShokupanApplicationTree = {};
1668
+ const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1669
+ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1670
+ RouteParamType2["BODY"] = "BODY";
1671
+ RouteParamType2["PARAM"] = "PARAM";
1672
+ RouteParamType2["QUERY"] = "QUERY";
1673
+ RouteParamType2["HEADER"] = "HEADER";
1674
+ RouteParamType2["REQUEST"] = "REQUEST";
1675
+ RouteParamType2["CONTEXT"] = "CONTEXT";
1676
+ return RouteParamType2;
1677
+ })(RouteParamType || {});
1691
1678
  class ShokupanRouter {
1692
1679
  constructor(config) {
1693
1680
  this.config = config;
@@ -1720,6 +1707,7 @@ class ShokupanRouter {
1720
1707
  metadata;
1721
1708
  // Metadata for the router itself
1722
1709
  currentGuards = [];
1710
+ eventHandlers = /* @__PURE__ */ new Map();
1723
1711
  // Registry Accessor
1724
1712
  getComponentRegistry() {
1725
1713
  const controllerRoutesMap = /* @__PURE__ */ new Map();
@@ -1781,233 +1769,54 @@ class ShokupanRouter {
1781
1769
  return typeof target === "object" && target !== null && $isRouter in target;
1782
1770
  }
1783
1771
  /**
1784
- * Mounts a controller instance to a path prefix.
1785
- *
1786
- * Controller can be a convection router or an arbitrary class.
1787
- *
1788
- * Routes are derived from method names:
1789
- * - get(ctx) -> GET /prefix/
1790
- * - getUsers(ctx) -> GET /prefix/users
1791
- * - postCreate(ctx) -> POST /prefix/create
1772
+ * Registers an event handler for WebSocket.
1792
1773
  */
1793
- mount(prefix, controller) {
1794
- const isRouter = this.isRouterInstance(controller);
1774
+ event(name, handler) {
1775
+ if (this.eventHandlers.has(name)) {
1776
+ const err = new EventError(`Event handler \`${name}\` already exists.`);
1777
+ console.warn(err);
1778
+ const handlers = this.eventHandlers.get(name);
1779
+ handlers.push(handler);
1780
+ this.eventHandlers.set(name, handlers);
1781
+ } else {
1782
+ this.eventHandlers.set(name, [handler]);
1783
+ }
1784
+ return this;
1785
+ }
1786
+ /**
1787
+ * Finds an event handler(s) by name.
1788
+ */
1789
+ findEvent(name) {
1790
+ if (this.eventHandlers.has(name)) {
1791
+ return this.eventHandlers.get(name);
1792
+ }
1793
+ for (const child of this[$childRouters]) {
1794
+ const handler = child.findEvent(name);
1795
+ if (handler) return handler;
1796
+ }
1797
+ return null;
1798
+ }
1799
+ /**
1800
+ * Mounts a controller instance to a path prefix.
1801
+ *
1802
+ * Controller can be a convection router or an arbitrary class.
1803
+ *
1804
+ * Routes are derived from method names:
1805
+ * - get(ctx) -> GET /prefix/
1806
+ * - getUsers(ctx) -> GET /prefix/users
1807
+ * - postCreate(ctx) -> POST /prefix/create
1808
+ */
1809
+ mount(prefix, controller) {
1810
+ const isRouter = this.isRouterInstance(controller);
1795
1811
  const isFunction = typeof controller === "function";
1796
1812
  const controllersOnly = this.config?.controllersOnly ?? this.rootConfig?.controllersOnly ?? false;
1797
1813
  if (controllersOnly && !isFunction && !isRouter) {
1798
1814
  throw new Error(`[Shokupan] strict controller check failed: ${controller.constructor.name || typeof controller} is not a class constructor.`);
1799
1815
  }
1800
1816
  if (this.isRouterInstance(controller)) {
1801
- if (controller[$isMounted]) {
1802
- throw new Error("Router is already mounted");
1803
- }
1804
- controller[$mountPath] = prefix;
1805
- if (!controller.metadata) {
1806
- const info = getCallerInfo();
1807
- controller.metadata = {
1808
- file: info.file,
1809
- line: info.line,
1810
- name: "MountedRouter"
1811
- };
1812
- }
1813
- this[$childRouters].push(controller);
1814
- controller[$parent] = this;
1815
- const setRouterContext = (router) => {
1816
- router[$appRoot] = this.root;
1817
- router[$childRouters].forEach((child) => setRouterContext(child));
1818
- };
1819
- setRouterContext(controller);
1820
- if (this[$appRoot]) ;
1821
- controller[$appRoot] = this.root;
1822
- controller[$isMounted] = true;
1817
+ this.mountRouter(prefix, controller);
1823
1818
  } else {
1824
- let instance = controller;
1825
- if (typeof controller === "function") {
1826
- instance = Container.resolve(controller);
1827
- const controllerPath = controller[$controllerPath];
1828
- if (controllerPath) {
1829
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1830
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1831
- prefix = p1 + p2;
1832
- if (!prefix) prefix = "/";
1833
- }
1834
- } else {
1835
- const ctor = instance.constructor;
1836
- const controllerPath = ctor[$controllerPath];
1837
- if (controllerPath) {
1838
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1839
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1840
- prefix = p1 + p2;
1841
- if (!prefix) prefix = "/";
1842
- }
1843
- }
1844
- instance[$mountPath] = prefix;
1845
- const info = getCallerInfo();
1846
- instance.metadata = {
1847
- file: info.file,
1848
- line: info.line,
1849
- name: instance.constructor.name
1850
- };
1851
- this[$childControllers].push(instance);
1852
- const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1853
- const proto = Object.getPrototypeOf(instance);
1854
- const methods = /* @__PURE__ */ new Set();
1855
- let current = proto;
1856
- while (current && current !== Object.prototype) {
1857
- Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1858
- current = Object.getPrototypeOf(current);
1859
- }
1860
- Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1861
- const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1862
- const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1863
- const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1864
- let routesAttached = 0;
1865
- for (let i = 0; i < Array.from(methods).length; i++) {
1866
- const name = Array.from(methods)[i];
1867
- if (name === "constructor") continue;
1868
- if (["arguments", "caller", "callee"].includes(name)) continue;
1869
- const originalHandler = instance[name];
1870
- if (typeof originalHandler !== "function") continue;
1871
- let method;
1872
- let subPath = "";
1873
- if (decoratedRoutes && decoratedRoutes.has(name)) {
1874
- const config = decoratedRoutes.get(name);
1875
- method = config.method;
1876
- subPath = config.path;
1877
- } else {
1878
- for (let j = 0; j < HTTPMethods.length; j++) {
1879
- const m = HTTPMethods[j];
1880
- if (name.toUpperCase().startsWith(m)) {
1881
- method = m;
1882
- const rest = name.slice(m.length);
1883
- if (rest.length === 0) {
1884
- subPath = "/";
1885
- } else {
1886
- subPath = "";
1887
- let buffer = "";
1888
- const flush = () => {
1889
- if (buffer.length > 0) {
1890
- subPath += "/" + buffer.toLowerCase();
1891
- buffer = "";
1892
- }
1893
- };
1894
- for (let i2 = 0; i2 < rest.length; i2++) {
1895
- const char = rest[i2];
1896
- if (char === "$") {
1897
- flush();
1898
- subPath += "/:";
1899
- continue;
1900
- }
1901
- }
1902
- subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1903
- if (!subPath.startsWith("/")) {
1904
- subPath = "/" + subPath;
1905
- }
1906
- }
1907
- break;
1908
- }
1909
- }
1910
- }
1911
- if (method) {
1912
- routesAttached++;
1913
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1914
- const cleanSubPath = subPath === "/" ? "" : subPath;
1915
- let joined;
1916
- if (cleanSubPath.length === 0) {
1917
- joined = cleanPrefix;
1918
- } else if (cleanSubPath.startsWith("/")) {
1919
- joined = cleanPrefix + cleanSubPath;
1920
- } else {
1921
- joined = cleanPrefix + "/" + cleanSubPath;
1922
- }
1923
- const fullPath = joined || "/";
1924
- const normalizedPath = fullPath.replace(/\/+/g, "/");
1925
- const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1926
- const allMiddleware = [...controllerMiddleware, ...methodMw];
1927
- const routeArgs = decoratedArgs && decoratedArgs.get(name);
1928
- const wrappedHandler = async (ctx) => {
1929
- let args = [ctx];
1930
- if (routeArgs?.length > 0) {
1931
- args = [];
1932
- const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1933
- for (let k = 0; k < sortedArgs.length; k++) {
1934
- const arg = sortedArgs[k];
1935
- switch (arg.type) {
1936
- case RouteParamType.BODY:
1937
- try {
1938
- if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1939
- args[arg.index] = await ctx.req.json();
1940
- } else {
1941
- const text = await ctx.req.text();
1942
- if (!text) {
1943
- args[arg.index] = {};
1944
- } else {
1945
- args[arg.index] = JSON.parse(text);
1946
- }
1947
- }
1948
- } catch (e) {
1949
- const err = new Error("Invalid JSON body");
1950
- err.status = 400;
1951
- throw err;
1952
- }
1953
- break;
1954
- case RouteParamType.PARAM:
1955
- args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1956
- break;
1957
- case RouteParamType.QUERY: {
1958
- const url = new URL(ctx.req.url);
1959
- if (arg.name) {
1960
- const vals = url.searchParams.getAll(arg.name);
1961
- args[arg.index] = vals.length > 1 ? vals : vals[0];
1962
- } else {
1963
- const query = {};
1964
- const keys = Object.keys(url.searchParams);
1965
- for (let k2 = 0; k2 < keys.length; k2++) {
1966
- const key = keys[k2];
1967
- const vals = url.searchParams.getAll(key);
1968
- query[key] = vals.length > 1 ? vals : vals[0];
1969
- }
1970
- args[arg.index] = query;
1971
- }
1972
- break;
1973
- }
1974
- case RouteParamType.HEADER:
1975
- args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
1976
- break;
1977
- case RouteParamType.REQUEST:
1978
- args[arg.index] = ctx.req;
1979
- break;
1980
- case RouteParamType.CONTEXT:
1981
- args[arg.index] = ctx;
1982
- break;
1983
- }
1984
- }
1985
- }
1986
- const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1987
- return tracedOriginalHandler.apply(instance, args);
1988
- };
1989
- let finalHandler = wrappedHandler;
1990
- if (allMiddleware.length > 0) {
1991
- const composed = compose(allMiddleware);
1992
- finalHandler = async (ctx) => {
1993
- return composed(ctx, () => wrappedHandler(ctx));
1994
- };
1995
- }
1996
- finalHandler.originalHandler = originalHandler;
1997
- if (finalHandler !== wrappedHandler) {
1998
- wrappedHandler.originalHandler = originalHandler;
1999
- }
2000
- const tagName = instance.constructor.name;
2001
- const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2002
- const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2003
- const spec = { tags: [tagName], ...userSpec };
2004
- this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2005
- }
2006
- }
2007
- if (routesAttached === 0) {
2008
- console.warn(`No routes attached to controller ${instance.constructor.name}`);
2009
- }
2010
- instance[$isMounted] = true;
1819
+ this.scanControllerRoutes(prefix, controller);
2011
1820
  }
2012
1821
  return this;
2013
1822
  }
@@ -2045,8 +1854,6 @@ class ShokupanRouter {
2045
1854
  */
2046
1855
  async internalRequest(arg) {
2047
1856
  const options = typeof arg === "string" ? { path: arg } : arg;
2048
- const store = asyncContext.getStore();
2049
- store?.get("req");
2050
1857
  let url = options.path;
2051
1858
  if (!url.startsWith("http")) {
2052
1859
  const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig.port || 3e3}`;
@@ -2089,7 +1896,7 @@ class ShokupanRouter {
2089
1896
  });
2090
1897
  const ctx = new ShokupanContext(req);
2091
1898
  let result = null;
2092
- let status = 200;
1899
+ let status = HTTP_STATUS.OK;
2093
1900
  const headers = {};
2094
1901
  const match = this.find(req.method, ctx.path);
2095
1902
  if (match) {
@@ -2098,12 +1905,12 @@ class ShokupanRouter {
2098
1905
  result = await match.handler(ctx);
2099
1906
  } catch (err) {
2100
1907
  console.error(err);
2101
- status = err.status || err.statusCode || 500;
1908
+ status = getErrorStatus(err);
2102
1909
  result = { error: err.message || "Internal Server Error" };
2103
1910
  if (err.errors) result.errors = err.errors;
2104
1911
  }
2105
1912
  } else {
2106
- status = 404;
1913
+ status = HTTP_STATUS.NOT_FOUND;
2107
1914
  result = "Not Found";
2108
1915
  }
2109
1916
  if (result instanceof Response) {
@@ -2132,7 +1939,7 @@ class ShokupanRouter {
2132
1939
  const originalHandler = handler;
2133
1940
  const wrapped = async (ctx) => {
2134
1941
  await this.runHooks("onRequestStart", ctx);
2135
- const debug = ctx._debug;
1942
+ const debug = ctx[$debug];
2136
1943
  let debugId;
2137
1944
  let previousNode;
2138
1945
  if (debug) {
@@ -2158,6 +1965,254 @@ class ShokupanRouter {
2158
1965
  wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
2159
1966
  return wrapped;
2160
1967
  }
1968
+ mountRouter(prefix, router) {
1969
+ if (router[$isMounted]) {
1970
+ throw new Error("Router is already mounted");
1971
+ }
1972
+ router[$mountPath] = prefix;
1973
+ if (!router.metadata) {
1974
+ const info = getCallerInfo();
1975
+ router.metadata = {
1976
+ file: info.file,
1977
+ line: info.line,
1978
+ name: "MountedRouter"
1979
+ };
1980
+ }
1981
+ this[$childRouters].push(router);
1982
+ router[$parent] = this;
1983
+ const setRouterContext = (router2) => {
1984
+ router2[$appRoot] = this.root;
1985
+ router2[$childRouters].forEach((child) => setRouterContext(child));
1986
+ };
1987
+ setRouterContext(router);
1988
+ router[$appRoot] = this.root;
1989
+ router[$isMounted] = true;
1990
+ }
1991
+ scanControllerRoutes(prefix, controller) {
1992
+ let instance = controller;
1993
+ if (typeof controller === "function") {
1994
+ instance = Container.resolve(controller);
1995
+ const controllerPath = controller[$controllerPath];
1996
+ if (controllerPath) {
1997
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1998
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1999
+ prefix = p1 + p2;
2000
+ if (!prefix) prefix = "/";
2001
+ }
2002
+ } else {
2003
+ const ctor = instance.constructor;
2004
+ const controllerPath = ctor[$controllerPath];
2005
+ if (controllerPath) {
2006
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2007
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
2008
+ prefix = p1 + p2;
2009
+ if (!prefix) prefix = "/";
2010
+ }
2011
+ }
2012
+ instance[$mountPath] = prefix;
2013
+ const info = getCallerInfo();
2014
+ instance.metadata = {
2015
+ file: info.file,
2016
+ line: info.line,
2017
+ name: instance.constructor.name
2018
+ };
2019
+ this[$childControllers].push(instance);
2020
+ const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
2021
+ const proto = Object.getPrototypeOf(instance);
2022
+ const methods = /* @__PURE__ */ new Set();
2023
+ let current = proto;
2024
+ while (current && current !== Object.prototype) {
2025
+ Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
2026
+ current = Object.getPrototypeOf(current);
2027
+ }
2028
+ Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
2029
+ const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
2030
+ const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
2031
+ const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
2032
+ const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
2033
+ let routesAttached = 0;
2034
+ for (let i = 0; i < Array.from(methods).length; i++) {
2035
+ const name = Array.from(methods)[i];
2036
+ if (name === "constructor") continue;
2037
+ if (["arguments", "caller", "callee"].includes(name)) continue;
2038
+ const originalHandler = instance[name];
2039
+ if (typeof originalHandler !== "function") continue;
2040
+ let method;
2041
+ let subPath = "";
2042
+ if (decoratedRoutes && decoratedRoutes.has(name)) {
2043
+ const config = decoratedRoutes.get(name);
2044
+ method = config.method;
2045
+ subPath = config.path;
2046
+ } else {
2047
+ for (let j = 0; j < HTTPMethods.length; j++) {
2048
+ const m = HTTPMethods[j];
2049
+ if (name.toUpperCase().startsWith(m)) {
2050
+ method = m;
2051
+ const rest = name.slice(m.length);
2052
+ if (rest.length === 0) {
2053
+ subPath = "/";
2054
+ } else {
2055
+ subPath = "";
2056
+ let buffer = "";
2057
+ const flush = () => {
2058
+ if (buffer.length > 0) {
2059
+ subPath += "/" + buffer.toLowerCase();
2060
+ buffer = "";
2061
+ }
2062
+ };
2063
+ for (let i2 = 0; i2 < rest.length; i2++) {
2064
+ const char = rest[i2];
2065
+ if (char === "$") {
2066
+ flush();
2067
+ subPath += "/:";
2068
+ continue;
2069
+ }
2070
+ buffer += char;
2071
+ }
2072
+ if (buffer.length > 0) flush();
2073
+ subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
2074
+ if (!subPath.startsWith("/")) {
2075
+ subPath = "/" + subPath;
2076
+ }
2077
+ }
2078
+ break;
2079
+ }
2080
+ }
2081
+ }
2082
+ if (method) {
2083
+ routesAttached++;
2084
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2085
+ const cleanSubPath = subPath === "/" ? "" : subPath;
2086
+ let joined;
2087
+ if (cleanSubPath.length === 0) {
2088
+ joined = cleanPrefix;
2089
+ } else if (cleanSubPath.startsWith("/")) {
2090
+ joined = cleanPrefix + cleanSubPath;
2091
+ } else {
2092
+ joined = cleanPrefix + "/" + cleanSubPath;
2093
+ }
2094
+ const fullPath = joined || "/";
2095
+ const normalizedPath = fullPath.replace(/\/+/g, "/");
2096
+ const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
2097
+ const allMiddleware = [...controllerMiddleware, ...methodMw];
2098
+ const routeArgs = decoratedArgs && decoratedArgs.get(name);
2099
+ const wrappedHandler = async (ctx) => {
2100
+ let args = [ctx];
2101
+ if (routeArgs?.length > 0) {
2102
+ args = [];
2103
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2104
+ for (let k = 0; k < sortedArgs.length; k++) {
2105
+ const arg = sortedArgs[k];
2106
+ switch (arg.type) {
2107
+ case RouteParamType.BODY:
2108
+ try {
2109
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
2110
+ args[arg.index] = await ctx.req.json();
2111
+ } else {
2112
+ const text = await ctx.req.text();
2113
+ if (!text) {
2114
+ args[arg.index] = {};
2115
+ } else {
2116
+ args[arg.index] = JSON.parse(text);
2117
+ }
2118
+ }
2119
+ } catch (e) {
2120
+ const err = new Error("Invalid JSON body");
2121
+ err.status = 400;
2122
+ throw err;
2123
+ }
2124
+ break;
2125
+ case RouteParamType.PARAM:
2126
+ args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
2127
+ break;
2128
+ case RouteParamType.QUERY: {
2129
+ const url = new URL(ctx.req.url);
2130
+ if (arg.name) {
2131
+ const vals = url.searchParams.getAll(arg.name);
2132
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
2133
+ } else {
2134
+ const query = {};
2135
+ const keys = Object.keys(url.searchParams);
2136
+ for (let k2 = 0; k2 < keys.length; k2++) {
2137
+ const key = keys[k2];
2138
+ const vals = url.searchParams.getAll(key);
2139
+ query[key] = vals.length > 1 ? vals : vals[0];
2140
+ }
2141
+ args[arg.index] = query;
2142
+ }
2143
+ break;
2144
+ }
2145
+ case RouteParamType.HEADER:
2146
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2147
+ break;
2148
+ case RouteParamType.REQUEST:
2149
+ args[arg.index] = ctx.req;
2150
+ break;
2151
+ case RouteParamType.CONTEXT:
2152
+ args[arg.index] = ctx;
2153
+ break;
2154
+ }
2155
+ }
2156
+ }
2157
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
2158
+ return tracedOriginalHandler.apply(instance, args);
2159
+ };
2160
+ let finalHandler = wrappedHandler;
2161
+ if (allMiddleware.length > 0) {
2162
+ const composed = compose(allMiddleware);
2163
+ finalHandler = async (ctx) => {
2164
+ return composed(ctx, () => wrappedHandler(ctx));
2165
+ };
2166
+ }
2167
+ finalHandler.originalHandler = originalHandler;
2168
+ if (finalHandler !== wrappedHandler) {
2169
+ wrappedHandler.originalHandler = originalHandler;
2170
+ }
2171
+ const tagName = instance.constructor.name;
2172
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2173
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2174
+ const spec = { tags: [tagName], ...userSpec };
2175
+ this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2176
+ }
2177
+ if (decoratedEvents?.has(name)) {
2178
+ routesAttached++;
2179
+ const config = decoratedEvents.get(name);
2180
+ const routeArgs = decoratedArgs?.get(name);
2181
+ const wrappedHandler = async (ctx) => {
2182
+ let args = [ctx];
2183
+ if (routeArgs?.length > 0) {
2184
+ args = [];
2185
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2186
+ for (let k = 0; k < sortedArgs.length; k++) {
2187
+ const arg = sortedArgs[k];
2188
+ switch (arg.type) {
2189
+ case RouteParamType.BODY:
2190
+ args[arg.index] = await ctx.body();
2191
+ break;
2192
+ case RouteParamType.CONTEXT:
2193
+ args[arg.index] = ctx;
2194
+ break;
2195
+ case RouteParamType.REQUEST:
2196
+ args[arg.index] = ctx.req;
2197
+ break;
2198
+ case RouteParamType.HEADER:
2199
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2200
+ break;
2201
+ default:
2202
+ args[arg.index] = void 0;
2203
+ }
2204
+ }
2205
+ }
2206
+ return originalHandler.apply(instance, args);
2207
+ };
2208
+ this.event(config.eventName, wrappedHandler);
2209
+ }
2210
+ }
2211
+ if (routesAttached === 0) {
2212
+ console.warn(`No routes attached to controller ${instance.constructor.name}`);
2213
+ }
2214
+ instance[$isMounted] = true;
2215
+ }
2161
2216
  /**
2162
2217
  * Find a route matching the given method and path.
2163
2218
  * @param method HTTP method
@@ -2284,7 +2339,7 @@ class ShokupanRouter {
2284
2339
  if (effectiveRenderer) {
2285
2340
  const innerHandler = wrappedHandler;
2286
2341
  wrappedHandler = async (ctx) => {
2287
- ctx.renderer = effectiveRenderer;
2342
+ ctx.setRenderer(effectiveRenderer);
2288
2343
  return innerHandler(ctx);
2289
2344
  };
2290
2345
  }
@@ -2315,8 +2370,10 @@ class ShokupanRouter {
2315
2370
  Promise.resolve().then(async () => {
2316
2371
  try {
2317
2372
  const timestamp = Date.now();
2318
- const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2319
- await datastore.set("middleware_tracking", key, {
2373
+ await datastore.set(new RecordId("middleware_tracking", {
2374
+ timestamp,
2375
+ name: handler.name || "anonymous"
2376
+ }), {
2320
2377
  name: handler.name || "anonymous",
2321
2378
  path: ctx.path,
2322
2379
  timestamp,
@@ -2334,7 +2391,7 @@ class ShokupanRouter {
2334
2391
  const cutoff = Date.now() - ttl;
2335
2392
  await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2336
2393
  const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2337
- if (results && results[0] && results[0].count > maxCapacity) {
2394
+ if (results?.[0]?.count > maxCapacity) {
2338
2395
  const toDelete = results[0].count - maxCapacity;
2339
2396
  await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2340
2397
  }
@@ -2411,7 +2468,7 @@ class ShokupanRouter {
2411
2468
  (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
2412
2469
  );
2413
2470
  if (callerLine) {
2414
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
2471
+ const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
2415
2472
  if (match) {
2416
2473
  file = match[1];
2417
2474
  line = parseInt(match[2], 10);
@@ -2429,7 +2486,7 @@ class ShokupanRouter {
2429
2486
  }
2430
2487
  return guardHandler(ctx, next);
2431
2488
  };
2432
- trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
2489
+ trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
2433
2490
  this.currentGuards.push({ handler: trackedGuard, spec });
2434
2491
  return this;
2435
2492
  }
@@ -2542,7 +2599,7 @@ class ShokupanRouter {
2542
2599
  const fns = this.hookCache.get(name);
2543
2600
  if (!fns) return;
2544
2601
  const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2545
- const debug = ctx?._debug;
2602
+ const debug = ctx?.[$debug];
2546
2603
  if (debug) {
2547
2604
  await Promise.all(fns.map(async (fn, index) => {
2548
2605
  const hookId = `hook_${name}_${fn.name || index}`;
@@ -2615,6 +2672,7 @@ const defaults = {
2615
2672
  hostname: "localhost",
2616
2673
  development: process.env.NODE_ENV !== "production",
2617
2674
  enableAsyncLocalStorage: false,
2675
+ enableHttpBridge: false,
2618
2676
  reusePort: false
2619
2677
  };
2620
2678
  trace.getTracer("shokupan.application");
@@ -2623,6 +2681,7 @@ class Shokupan extends ShokupanRouter {
2623
2681
  openApiSpec;
2624
2682
  composedMiddleware;
2625
2683
  cpuMonitor;
2684
+ server;
2626
2685
  get logger() {
2627
2686
  return this.applicationConfig.logger;
2628
2687
  }
@@ -2686,6 +2745,13 @@ class Shokupan extends ShokupanRouter {
2686
2745
  }
2687
2746
  return this;
2688
2747
  }
2748
+ /**
2749
+ * Registers a plugin.
2750
+ */
2751
+ register(plugin, options) {
2752
+ plugin.onInit(this, options);
2753
+ return this;
2754
+ }
2689
2755
  startupHooks = [];
2690
2756
  /**
2691
2757
  * Registers a callback to be executed before the server starts listening.
@@ -2724,6 +2790,7 @@ class Shokupan extends ShokupanRouter {
2724
2790
  this.cpuMonitor = new SystemCpuMonitor();
2725
2791
  this.cpuMonitor.start();
2726
2792
  }
2793
+ const self = this;
2727
2794
  const serveOptions = {
2728
2795
  port: finalPort,
2729
2796
  hostname: this.applicationConfig.hostname,
@@ -2735,8 +2802,61 @@ class Shokupan extends ShokupanRouter {
2735
2802
  open(ws) {
2736
2803
  ws.data?.handler?.open?.(ws);
2737
2804
  },
2738
- message(ws, message) {
2739
- ws.data?.handler?.message?.(ws, message);
2805
+ async message(ws, message) {
2806
+ if (ws.data?.handler?.message) {
2807
+ return ws.data.handler.message(ws, message);
2808
+ }
2809
+ if (typeof message !== "string") return;
2810
+ try {
2811
+ const payload = JSON.parse(message);
2812
+ if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
2813
+ const { id, method, path, headers, body } = payload;
2814
+ const url = new URL(path, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
2815
+ const req = new Request(url.toString(), {
2816
+ method,
2817
+ headers,
2818
+ body: typeof body === "object" ? JSON.stringify(body) : body
2819
+ });
2820
+ const res = await self.fetch(req);
2821
+ const resBody = await res.json().catch((err) => res.text());
2822
+ const resHeaders = {};
2823
+ res.headers.forEach((v, k) => resHeaders[k] = v);
2824
+ ws.send(JSON.stringify({
2825
+ type: "RESPONSE",
2826
+ id,
2827
+ status: res.status,
2828
+ headers: resHeaders,
2829
+ body: resBody
2830
+ }));
2831
+ return;
2832
+ }
2833
+ const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
2834
+ if (eventName) {
2835
+ const handlers = self.findEvent(eventName);
2836
+ const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
2837
+ if (handler) {
2838
+ const data = payload.data || payload.payload;
2839
+ const req = new ShokupanRequest({
2840
+ url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
2841
+ method: "POST",
2842
+ headers: new Headers({ "content-type": "application/json" }),
2843
+ body: JSON.stringify(data)
2844
+ });
2845
+ const ctx = new ShokupanContext(req, self.server);
2846
+ ctx[$ws] = ws;
2847
+ try {
2848
+ await handler(ctx);
2849
+ } catch (err) {
2850
+ if (self.applicationConfig["websocketErrorHandler"]) {
2851
+ await self.applicationConfig["websocketErrorHandler"](err, ctx);
2852
+ } else {
2853
+ console.error(`Error in event ${eventName}:`, err);
2854
+ }
2855
+ }
2856
+ }
2857
+ }
2858
+ } catch (e) {
2859
+ }
2740
2860
  },
2741
2861
  drain(ws) {
2742
2862
  ws.data?.handler?.drain?.(ws);
@@ -2748,12 +2868,40 @@ class Shokupan extends ShokupanRouter {
2748
2868
  };
2749
2869
  let factory = this.applicationConfig.serverFactory;
2750
2870
  if (!factory && typeof Bun === "undefined") {
2751
- const { createHttpServer } = await import("./server-adapter-0xH174zz.js");
2871
+ const { createHttpServer } = await import("./http-server-CCeagTyU.js");
2752
2872
  factory = createHttpServer();
2753
2873
  }
2754
- const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2874
+ this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2755
2875
  console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
2756
- return server;
2876
+ return this.server;
2877
+ }
2878
+ /**
2879
+ * Stops the application server.
2880
+ *
2881
+ * This method gracefully shuts down the server and stops any running monitors.
2882
+ * Works transparently in both Bun and Node.js runtimes.
2883
+ *
2884
+ * @returns A promise that resolves when the server has been stopped.
2885
+ *
2886
+ * @example
2887
+ * ```typescript
2888
+ * const app = new Shokupan();
2889
+ * const server = await app.listen(3000);
2890
+ *
2891
+ * // Later, when you want to stop the server
2892
+ * await app.stop();
2893
+ * ```
2894
+ * @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
2895
+ */
2896
+ async stop(closeActiveConnections) {
2897
+ if (this.cpuMonitor) {
2898
+ this.cpuMonitor.stop();
2899
+ this.cpuMonitor = void 0;
2900
+ }
2901
+ if (this.server) {
2902
+ await this.server.stop(closeActiveConnections);
2903
+ this.server = void 0;
2904
+ }
2757
2905
  }
2758
2906
  [$dispatch](req) {
2759
2907
  return this.fetch(req);
@@ -2817,19 +2965,19 @@ class Shokupan extends ShokupanRouter {
2817
2965
  "http.method": req.method
2818
2966
  }
2819
2967
  };
2820
- const parent = store?.get("span");
2968
+ const parent = store?.span;
2821
2969
  const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
2822
2970
  return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2823
- const ctxMap = /* @__PURE__ */ new Map();
2824
- ctxMap.set("span", span);
2825
- ctxMap.set("request", req);
2826
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
2971
+ const ctxStore = new RequestContextStore();
2972
+ ctxStore.span = span;
2973
+ ctxStore.request = req;
2974
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server).finally(() => span.end()));
2827
2975
  });
2828
2976
  }
2829
2977
  if (this.applicationConfig.enableAsyncLocalStorage) {
2830
- const ctxMap = /* @__PURE__ */ new Map();
2831
- ctxMap.set("request", req);
2832
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
2978
+ const ctxStore = new RequestContextStore();
2979
+ ctxStore.request = req;
2980
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server));
2833
2981
  }
2834
2982
  return this.handleRequest(req, server);
2835
2983
  }
@@ -2851,24 +2999,34 @@ class Shokupan extends ShokupanRouter {
2851
2999
  const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2852
3000
  const match = this.find(req.method, ctx.path);
2853
3001
  if (match) {
3002
+ ctx[$routeMatched] = true;
2854
3003
  ctx.params = match.params;
2855
3004
  await bodyParsing;
2856
3005
  return match.handler(ctx);
2857
3006
  }
3007
+ if (ctx.upgrade()) {
3008
+ return void 0;
3009
+ }
2858
3010
  return null;
2859
3011
  });
2860
3012
  let response;
2861
3013
  if (result instanceof Response) {
2862
3014
  response = result;
2863
- } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2864
- response = ctx._finalResponse;
3015
+ } else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
3016
+ response = ctx[$finalResponse];
2865
3017
  } else if (result === null || result === void 0) {
2866
- if (ctx._finalResponse instanceof Response) {
2867
- response = ctx._finalResponse;
2868
- } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
3018
+ if (ctx[$finalResponse] instanceof Response) {
3019
+ response = ctx[$finalResponse];
3020
+ } else if (ctx.isUpgraded) {
3021
+ return void 0;
3022
+ } else if (ctx[$routeMatched]) {
2869
3023
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2870
3024
  } else {
2871
- response = ctx.text("Not Found", 404);
3025
+ if (ctx.response.status !== HTTP_STATUS.OK) {
3026
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
3027
+ } else {
3028
+ response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
3029
+ }
2872
3030
  }
2873
3031
  } else if (typeof result === "object") {
2874
3032
  response = ctx.json(result);
@@ -2879,10 +3037,9 @@ class Shokupan extends ShokupanRouter {
2879
3037
  await this.runHooks("onResponseStart", ctx, response);
2880
3038
  return response;
2881
3039
  } catch (err) {
2882
- console.error(err);
2883
- const span = asyncContext.getStore()?.get("span");
3040
+ const span = asyncContext.getStore()?.span;
2884
3041
  if (span) span.setStatus({ code: 2 });
2885
- const status = err.status || err.statusCode || 500;
3042
+ const status = getErrorStatus(err);
2886
3043
  const body = { error: err.message || "Internal Server Error" };
2887
3044
  if (err.errors) body.errors = err.errors;
2888
3045
  await this.runHooks("onError", ctx, err);
@@ -2904,16 +3061,188 @@ class Shokupan extends ShokupanRouter {
2904
3061
  }
2905
3062
  return executionPromise.catch((err) => {
2906
3063
  if (err.message === "Request Timeout") {
2907
- return ctx.text("Request Timeout", 408);
3064
+ return ctx.text("Request Timeout", HTTP_STATUS.REQUEST_TIMEOUT);
2908
3065
  }
2909
3066
  console.error("Unexpected error in request execution:", err);
2910
- return ctx.text("Internal Server Error", 500);
3067
+ return ctx.text("Internal Server Error", HTTP_STATUS.INTERNAL_SERVER_ERROR);
2911
3068
  }).then(async (res) => {
2912
3069
  await this.runHooks("onResponseEnd", ctx, res);
2913
3070
  return res;
2914
3071
  });
2915
3072
  }
2916
3073
  }
3074
+ function RateLimitMiddleware(options = {}) {
3075
+ const windowMs = options.windowMs || 60 * 1e3;
3076
+ const max = options.limit || options.max || 5;
3077
+ const message = options.message || "Too many requests, please try again later.";
3078
+ const statusCode = options.statusCode || 429;
3079
+ const headers = options.headers !== false;
3080
+ const mode = options.mode || "user";
3081
+ const trustedProxies = options.trustedProxies || [];
3082
+ const keyGenerator = options.keyGenerator || ((ctx) => {
3083
+ if (mode === "absolute") {
3084
+ return "global";
3085
+ }
3086
+ const xForwardedFor = ctx.headers.get("x-forwarded-for");
3087
+ if (xForwardedFor && trustedProxies.length > 0) {
3088
+ const ips = xForwardedFor.split(",").map((ip) => ip.trim());
3089
+ for (let i = ips.length - 1; i >= 0; i--) {
3090
+ const ip = ips[i];
3091
+ if (!trustedProxies.includes(ip)) {
3092
+ if (/^[\d.:a-fA-F]+$/.test(ip)) {
3093
+ return ip;
3094
+ }
3095
+ }
3096
+ }
3097
+ }
3098
+ return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
3099
+ });
3100
+ const skip = options.skip || (() => false);
3101
+ const hits = /* @__PURE__ */ new Map();
3102
+ const interval = setInterval(() => {
3103
+ const now = Date.now();
3104
+ const entries = Array.from(hits.entries());
3105
+ for (let i = 0; i < entries.length; i++) {
3106
+ const [key, record] = entries[i];
3107
+ if (record.resetTime <= now) {
3108
+ hits.delete(key);
3109
+ }
3110
+ }
3111
+ }, windowMs);
3112
+ if (interval.unref) interval.unref();
3113
+ const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
3114
+ if (skip(ctx)) return next();
3115
+ const key = keyGenerator(ctx);
3116
+ const now = Date.now();
3117
+ let record = hits.get(key);
3118
+ if (!record || record.resetTime <= now) {
3119
+ record = {
3120
+ hits: 0,
3121
+ resetTime: now + windowMs
3122
+ };
3123
+ hits.set(key, record);
3124
+ }
3125
+ record.hits++;
3126
+ const remaining = Math.max(0, max - record.hits);
3127
+ const resetTime = Math.ceil(record.resetTime / 1e3);
3128
+ const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
3129
+ const setHeaders = (res) => {
3130
+ if (!headers || !res || !res.headers) return;
3131
+ try {
3132
+ res.headers.set("X-RateLimit-Limit", String(max));
3133
+ res.headers.set("X-RateLimit-Remaining", String(remaining));
3134
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
3135
+ } catch (e) {
3136
+ }
3137
+ };
3138
+ if (record.hits > max) {
3139
+ if (options.onRateLimited) {
3140
+ const result = await options.onRateLimited(ctx, key);
3141
+ if (result instanceof Response) {
3142
+ return result;
3143
+ }
3144
+ }
3145
+ const msg = typeof message === "function" ? message(ctx, key) : message;
3146
+ typeof msg === "object" ? JSON.stringify(msg) : String(msg);
3147
+ const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
3148
+ if (headers) {
3149
+ setHeaders(res);
3150
+ res.headers.set("Retry-After", String(retryAfter));
3151
+ }
3152
+ return res;
3153
+ }
3154
+ const response = await next();
3155
+ if (response instanceof Response && headers) {
3156
+ setHeaders(response);
3157
+ }
3158
+ return response;
3159
+ };
3160
+ rateLimitMiddleware.isBuiltin = true;
3161
+ rateLimitMiddleware.pluginName = "RateLimit";
3162
+ return rateLimitMiddleware;
3163
+ }
3164
+ function Controller(path = "/") {
3165
+ return (target) => {
3166
+ target[$controllerPath] = path;
3167
+ };
3168
+ }
3169
+ function Use(...middleware) {
3170
+ return (target, propertyKey, descriptor) => {
3171
+ if (!propertyKey) {
3172
+ const existing = target[$middleware] || [];
3173
+ target[$middleware] = [...existing, ...middleware];
3174
+ } else {
3175
+ if (!target[$middleware]) {
3176
+ target[$middleware] = /* @__PURE__ */ new Map();
3177
+ }
3178
+ const existing = target[$middleware].get(propertyKey) || [];
3179
+ target[$middleware].set(propertyKey, [...existing, ...middleware]);
3180
+ }
3181
+ };
3182
+ }
3183
+ function createParamDecorator(type) {
3184
+ return (name) => {
3185
+ return (target, propertyKey, parameterIndex) => {
3186
+ if (!target[$routeArgs]) {
3187
+ target[$routeArgs] = /* @__PURE__ */ new Map();
3188
+ }
3189
+ if (!target[$routeArgs].has(propertyKey)) {
3190
+ target[$routeArgs].set(propertyKey, []);
3191
+ }
3192
+ target[$routeArgs].get(propertyKey).push({
3193
+ index: parameterIndex,
3194
+ type,
3195
+ name
3196
+ });
3197
+ };
3198
+ };
3199
+ }
3200
+ const Body = createParamDecorator(RouteParamType.BODY);
3201
+ const Param = createParamDecorator(RouteParamType.PARAM);
3202
+ const Query = createParamDecorator(RouteParamType.QUERY);
3203
+ const Headers$1 = createParamDecorator(RouteParamType.HEADER);
3204
+ const Req = createParamDecorator(RouteParamType.REQUEST);
3205
+ const Ctx = createParamDecorator(RouteParamType.CONTEXT);
3206
+ function Spec(spec) {
3207
+ return (target, propertyKey, descriptor) => {
3208
+ if (!target[$routeSpec]) {
3209
+ target[$routeSpec] = /* @__PURE__ */ new Map();
3210
+ }
3211
+ target[$routeSpec].set(propertyKey, spec);
3212
+ };
3213
+ }
3214
+ function createMethodDecorator(method) {
3215
+ return (path = "/") => {
3216
+ return (target, propertyKey, descriptor) => {
3217
+ if (!target[$routeMethods]) {
3218
+ target[$routeMethods] = /* @__PURE__ */ new Map();
3219
+ }
3220
+ target[$routeMethods].set(propertyKey, {
3221
+ method,
3222
+ path
3223
+ });
3224
+ };
3225
+ };
3226
+ }
3227
+ const Get = createMethodDecorator("GET");
3228
+ const Post = createMethodDecorator("POST");
3229
+ const Put = createMethodDecorator("PUT");
3230
+ const Delete = createMethodDecorator("DELETE");
3231
+ const Patch = createMethodDecorator("PATCH");
3232
+ const Options = createMethodDecorator("OPTIONS");
3233
+ const Head = createMethodDecorator("HEAD");
3234
+ const All = createMethodDecorator("ALL");
3235
+ function Event(eventName) {
3236
+ return (target, propertyKey, descriptor) => {
3237
+ target[$eventMethods] ??= /* @__PURE__ */ new Map();
3238
+ target[$eventMethods].set(propertyKey, {
3239
+ eventName
3240
+ });
3241
+ };
3242
+ }
3243
+ function RateLimit(options) {
3244
+ return Use(RateLimitMiddleware(options));
3245
+ }
2917
3246
  class AuthPlugin extends ShokupanRouter {
2918
3247
  constructor(authConfig) {
2919
3248
  super();
@@ -2922,6 +3251,13 @@ class AuthPlugin extends ShokupanRouter {
2922
3251
  this.init();
2923
3252
  }
2924
3253
  secret;
3254
+ onInit(app, options) {
3255
+ if (options?.path) {
3256
+ app.mount(options.path, this);
3257
+ } else {
3258
+ app.mount(options.path ?? "/", this);
3259
+ }
3260
+ }
2925
3261
  getProviderInstance(name, p) {
2926
3262
  switch (name) {
2927
3263
  case "github":
@@ -3074,73 +3410,809 @@ class AuthPlugin extends ShokupanRouter {
3074
3410
  provider,
3075
3411
  raw: data
3076
3412
  };
3077
- } else if (provider === "auth0" || provider === "okta") {
3078
- const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
3079
- const endpoint = provider === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
3080
- const res = await fetch(endpoint, {
3081
- headers: { Authorization: `Bearer ${token}` }
3413
+ } else if (provider === "auth0" || provider === "okta") {
3414
+ const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
3415
+ const endpoint = provider === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
3416
+ const res = await fetch(endpoint, {
3417
+ headers: { Authorization: `Bearer ${token}` }
3418
+ });
3419
+ const data = await res.json();
3420
+ user = {
3421
+ id: data.sub,
3422
+ name: data.name,
3423
+ email: data.email,
3424
+ picture: data.picture,
3425
+ provider,
3426
+ raw: data
3427
+ };
3428
+ } else if (provider === "apple") {
3429
+ if (idToken) {
3430
+ const payload = jose.decodeJwt(idToken);
3431
+ user = {
3432
+ id: payload.sub,
3433
+ email: payload["email"],
3434
+ provider,
3435
+ raw: payload
3436
+ };
3437
+ }
3438
+ } else if (provider === "oauth2") {
3439
+ if (config.userInfoUrl) {
3440
+ const res = await fetch(config.userInfoUrl, {
3441
+ headers: { Authorization: `Bearer ${token}` }
3442
+ });
3443
+ const data = await res.json();
3444
+ user = {
3445
+ id: data.id || data.sub || "unknown",
3446
+ name: data.name,
3447
+ email: data.email,
3448
+ picture: data.picture,
3449
+ provider,
3450
+ raw: data
3451
+ };
3452
+ }
3453
+ }
3454
+ return user;
3455
+ }
3456
+ /**
3457
+ * Middleware to verify JWT
3458
+ */
3459
+ getMiddleware() {
3460
+ return async (ctx, next) => {
3461
+ const authHeader = ctx.req.headers.get("Authorization");
3462
+ let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
3463
+ if (!token) {
3464
+ const cookieHeader = ctx.req.headers.get("Cookie");
3465
+ token = cookieHeader?.match(/auth_token=([^;]+)/)?.[1] || null;
3466
+ }
3467
+ if (token) {
3468
+ try {
3469
+ const { payload } = await jose.jwtVerify(token, this.secret);
3470
+ ctx.user = payload;
3471
+ } catch {
3472
+ }
3473
+ }
3474
+ return next();
3475
+ };
3476
+ }
3477
+ }
3478
+ class ClusterPlugin {
3479
+ constructor(options = {}) {
3480
+ this.options = options;
3481
+ }
3482
+ onInit(app) {
3483
+ const originalListen = app.listen.bind(app);
3484
+ const { workers = "auto", silent = false, sticky = false } = this.options;
3485
+ const isBun = typeof Bun !== "undefined";
3486
+ const numCPUs = os__default.cpus().length;
3487
+ const numWorkers = workers === "auto" || workers === -1 ? numCPUs : workers;
3488
+ if (numWorkers <= 1) {
3489
+ return;
3490
+ }
3491
+ app.listen = async (port) => {
3492
+ const finalPort = port ?? app.applicationConfig.port ?? 3e3;
3493
+ if (isBun) {
3494
+ return this.handleBun(app, finalPort, numWorkers, originalListen);
3495
+ } else {
3496
+ return this.handleNode(app, finalPort, numWorkers, originalListen, silent, sticky);
3497
+ }
3498
+ };
3499
+ }
3500
+ async handleBun(app, port, workers, originalListen) {
3501
+ const workerId = process.env["SHOKUPAN_WORKER_ID"];
3502
+ if (workerId) {
3503
+ app.applicationConfig.reusePort = true;
3504
+ return originalListen(port);
3505
+ }
3506
+ console.log(`[Cluster] Starting ${workers} Bun workers on port ${port}...`);
3507
+ const spawnWorker = (id) => {
3508
+ Bun.spawn([process.argv0, ...process.argv.slice(1)], {
3509
+ env: { ...process.env, SHOKUPAN_WORKER_ID: id },
3510
+ stdio: ["inherit", "inherit", "inherit"],
3511
+ onExit(proc, exitCode, signalCode, error) {
3512
+ console.log(`[Cluster] Worker ${id} died (code: ${exitCode}). Restarting...`);
3513
+ spawnWorker(id);
3514
+ }
3515
+ });
3516
+ };
3517
+ for (let i = 0; i < workers; i++) {
3518
+ spawnWorker(process.pid + "_" + i + 1);
3519
+ }
3520
+ setInterval(() => {
3521
+ }, 1e3 * 60 * 60);
3522
+ return {
3523
+ stop: () => {
3524
+ },
3525
+ port
3526
+ };
3527
+ }
3528
+ async handleNode(app, port, workers, originalListen, silent, sticky) {
3529
+ if (cluster.isPrimary) {
3530
+ console.log(`[Cluster] Master ${process.pid} is running`);
3531
+ const fork = () => cluster.fork(process.env);
3532
+ for (let i = 0; i < workers; i++) {
3533
+ fork();
3534
+ }
3535
+ cluster.on("exit", (worker, code, signal) => {
3536
+ console.log(`[Cluster] Worker ${worker.process.pid} died. Restarting...`);
3537
+ fork();
3538
+ });
3539
+ if (sticky) {
3540
+ const server = net.createServer({ pauseOnConnect: true }, (connection) => {
3541
+ const remote = connection.remoteAddress || "";
3542
+ let hash = 0;
3543
+ for (let i = 0; i < remote.length; i++) {
3544
+ hash = (hash << 5) - hash + remote.charCodeAt(i);
3545
+ hash |= 0;
3546
+ }
3547
+ const index = Math.abs(hash) % workers;
3548
+ const worker = Object.values(cluster.workers)[index];
3549
+ if (worker) {
3550
+ worker.send("sticky-session:connection", connection);
3551
+ } else {
3552
+ connection.end();
3553
+ }
3554
+ });
3555
+ server.listen(port, () => {
3556
+ console.log(`[Cluster] Sticky Load Balancer listening on port ${port}`);
3557
+ });
3558
+ return {
3559
+ close: () => server.close(),
3560
+ port
3561
+ };
3562
+ } else {
3563
+ return {
3564
+ close: () => {
3565
+ },
3566
+ // Master controls
3567
+ port
3568
+ };
3569
+ }
3570
+ } else {
3571
+ if (sticky) {
3572
+ const server = await originalListen(0);
3573
+ process.on("message", (message, handle) => {
3574
+ if (message !== "sticky-session:connection") return;
3575
+ if (!handle) return;
3576
+ server.emit("connection", handle);
3577
+ handle.resume();
3578
+ });
3579
+ return server;
3580
+ } else {
3581
+ return originalListen(port);
3582
+ }
3583
+ }
3584
+ }
3585
+ }
3586
+ const INTERVALS = [
3587
+ { label: "10s", ms: 10 * 1e3 },
3588
+ { label: "1m", ms: 60 * 1e3 },
3589
+ { label: "5m", ms: 5 * 60 * 1e3 },
3590
+ { label: "1h", ms: 60 * 60 * 1e3 },
3591
+ { label: "2h", ms: 2 * 60 * 60 * 1e3 },
3592
+ { label: "6h", ms: 6 * 60 * 60 * 1e3 },
3593
+ { label: "12h", ms: 12 * 60 * 60 * 1e3 },
3594
+ { label: "1d", ms: 24 * 60 * 60 * 1e3 },
3595
+ { label: "3d", ms: 3 * 24 * 60 * 60 * 1e3 },
3596
+ { label: "7d", ms: 7 * 24 * 60 * 60 * 1e3 },
3597
+ { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
3598
+ ];
3599
+ class MetricsCollector {
3600
+ currentIntervalStart = {};
3601
+ pendingDetails = {};
3602
+ eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
3603
+ timer = null;
3604
+ constructor() {
3605
+ this.eventLoopHistogram.enable();
3606
+ const now = Date.now();
3607
+ INTERVALS.forEach((int) => {
3608
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3609
+ this.pendingDetails[int.label] = [];
3610
+ });
3611
+ this.timer = setInterval(() => this.collect(), 1e4);
3612
+ }
3613
+ recordRequest(duration, isError) {
3614
+ INTERVALS.forEach((int) => {
3615
+ this.pendingDetails[int.label].push({ duration, isError });
3616
+ });
3617
+ }
3618
+ alignTimestamp(ts, intervalMs) {
3619
+ return Math.floor(ts / intervalMs) * intervalMs;
3620
+ }
3621
+ async collect() {
3622
+ try {
3623
+ const now = Date.now();
3624
+ console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
3625
+ for (const int of INTERVALS) {
3626
+ const start = this.currentIntervalStart[int.label];
3627
+ if (now >= start + int.ms) {
3628
+ console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
3629
+ await this.flushInterval(int.label, start, int.ms);
3630
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3631
+ }
3632
+ }
3633
+ } catch (error) {
3634
+ console.error("[MetricsCollector] Error in collect():", error);
3635
+ }
3636
+ }
3637
+ async flushInterval(label, timestamp, durationMs) {
3638
+ const reqs = this.pendingDetails[label];
3639
+ console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
3640
+ this.pendingDetails[label] = [];
3641
+ if (reqs.length === 0) {
3642
+ console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
3643
+ return;
3644
+ }
3645
+ const totalReqs = reqs.length;
3646
+ const errorReqs = reqs.filter((r) => r.isError).length;
3647
+ const successReqs = totalReqs - errorReqs;
3648
+ const duratons = reqs.map((r) => r.duration).sort((a, b) => a - b);
3649
+ const rps = totalReqs / (durationMs / 1e3);
3650
+ const sum = duratons.reduce((a, b) => a + b, 0);
3651
+ const avg = totalReqs > 0 ? sum / totalReqs : 0;
3652
+ const getP = (p) => {
3653
+ if (duratons.length === 0) return 0;
3654
+ const idx = Math.floor(duratons.length * p);
3655
+ return duratons[idx];
3656
+ };
3657
+ const metric = {
3658
+ timestamp,
3659
+ interval: label,
3660
+ cpu: os.loadavg()[0],
3661
+ // Using load avg for simplicity as per requirements (Load)
3662
+ load: os.loadavg(),
3663
+ memory: {
3664
+ used: process.memoryUsage().rss,
3665
+ total: os.totalmem(),
3666
+ heapUsed: process.memoryUsage().heapUsed,
3667
+ heapTotal: process.memoryUsage().heapTotal
3668
+ },
3669
+ eventLoopLatency: {
3670
+ min: this.eventLoopHistogram.min / 1e6,
3671
+ max: this.eventLoopHistogram.max / 1e6,
3672
+ mean: this.eventLoopHistogram.mean / 1e6,
3673
+ p50: this.eventLoopHistogram.percentile(50) / 1e6,
3674
+ p95: this.eventLoopHistogram.percentile(95) / 1e6,
3675
+ p99: this.eventLoopHistogram.percentile(99) / 1e6
3676
+ },
3677
+ requests: {
3678
+ total: totalReqs,
3679
+ rps,
3680
+ success: successReqs,
3681
+ error: errorReqs
3682
+ },
3683
+ responseTime: {
3684
+ min: duratons[0] || 0,
3685
+ max: duratons[duratons.length - 1] || 0,
3686
+ avg,
3687
+ p50: getP(0.5),
3688
+ p95: getP(0.95),
3689
+ p99: getP(0.99)
3690
+ }
3691
+ };
3692
+ console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
3693
+ try {
3694
+ const recordId = new RecordId("metrics", timestamp);
3695
+ await datastore.set(recordId, metric);
3696
+ console.log(`[MetricsCollector] ✓ Successfully saved ${label} metric to datastore`);
3697
+ const test = await datastore.get(recordId);
3698
+ console.log(`[MetricsCollector] DEBUG: Immediate .get() returned:`, test ? "DATA" : "NULL");
3699
+ const queryTest = await datastore.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
3700
+ console.log(`[MetricsCollector] DEBUG: Query by id returned ${queryTest[0]?.length || 0} records`);
3701
+ } catch (e) {
3702
+ console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
3703
+ }
3704
+ }
3705
+ // Cleanup if needed
3706
+ stop() {
3707
+ if (this.timer) clearInterval(this.timer);
3708
+ this.eventLoopHistogram.disable();
3709
+ }
3710
+ }
3711
+ class Collector {
3712
+ constructor(dashboard) {
3713
+ this.dashboard = dashboard;
3714
+ }
3715
+ currentNode;
3716
+ trackStep(id, type, duration, status, error) {
3717
+ if (!id) return;
3718
+ this.dashboard.recordNodeMetric(id, type, duration, status === "error");
3719
+ }
3720
+ trackEdge(fromId, toId) {
3721
+ if (!fromId || !toId) return;
3722
+ this.dashboard.recordEdgeMetric(fromId, toId);
3723
+ }
3724
+ setNode(id) {
3725
+ this.currentNode = id;
3726
+ }
3727
+ getCurrentNode() {
3728
+ return this.currentNode;
3729
+ }
3730
+ }
3731
+ class Dashboard {
3732
+ constructor(dashboardConfig = {}) {
3733
+ this.dashboardConfig = dashboardConfig;
3734
+ }
3735
+ static __dirname = dirname(fileURLToPath(import.meta.url));
3736
+ // Get base path for dashboard files - works in both dev (src/) and production (dist/)
3737
+ static getBasePath() {
3738
+ const dir = dirname(fileURLToPath(import.meta.url));
3739
+ if (dir.endsWith("dist")) {
3740
+ return dir + "/plugins/application/dashboard";
3741
+ }
3742
+ return dir;
3743
+ }
3744
+ router = new ShokupanRouter();
3745
+ metrics = {
3746
+ totalRequests: 0,
3747
+ successfulRequests: 0,
3748
+ failedRequests: 0,
3749
+ activeRequests: 0,
3750
+ averageTotalTime_ms: 0,
3751
+ recentTimings: [],
3752
+ logs: [],
3753
+ rateLimitedCounts: {},
3754
+ nodeMetrics: {},
3755
+ edgeMetrics: {}
3756
+ };
3757
+ eta = new Eta({
3758
+ views: Dashboard.getBasePath() + "/static",
3759
+ cache: false
3760
+ });
3761
+ startTime = Date.now();
3762
+ instrumented = false;
3763
+ metricsCollector = new MetricsCollector();
3764
+ // ShokupanPlugin interface implementation
3765
+ onInit(app, options) {
3766
+ this[$appRoot] = app;
3767
+ const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
3768
+ const hooks = this.getHooks();
3769
+ if (!app.middleware) {
3770
+ app.middleware = [];
3771
+ }
3772
+ const hooksMiddleware = async (ctx, next) => {
3773
+ if (hooks.onRequestStart) {
3774
+ await hooks.onRequestStart(ctx);
3775
+ }
3776
+ await next();
3777
+ if (hooks.onResponseEnd) {
3778
+ const effectiveResponse = ctx._finalResponse || ctx.response || {};
3779
+ await hooks.onResponseEnd(ctx, effectiveResponse);
3780
+ }
3781
+ };
3782
+ app.use(hooksMiddleware);
3783
+ app.mount(mountPath, this.router);
3784
+ this.setupRoutes();
3785
+ }
3786
+ setupRoutes() {
3787
+ this.router.get("/metrics", async (ctx) => {
3788
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
3789
+ const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
3790
+ const interval = ctx.query["interval"];
3791
+ if (interval) {
3792
+ const intervalMap = {
3793
+ "10s": 10 * 1e3,
3794
+ "1m": 60 * 1e3,
3795
+ "5m": 5 * 60 * 1e3,
3796
+ "30m": 30 * 60 * 1e3,
3797
+ "1h": 60 * 60 * 1e3,
3798
+ "2h": 2 * 60 * 60 * 1e3,
3799
+ "6h": 6 * 60 * 60 * 1e3,
3800
+ "12h": 12 * 60 * 60 * 1e3,
3801
+ "1d": 24 * 60 * 60 * 1e3,
3802
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3803
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3804
+ "30d": 30 * 24 * 60 * 60 * 1e3
3805
+ };
3806
+ const ms = intervalMap[interval] || 60 * 1e3;
3807
+ const startTime = Date.now() - ms;
3808
+ let stats;
3809
+ try {
3810
+ stats = await datastore.query(`
3811
+ SELECT
3812
+ count() as total,
3813
+ count(IF status < 400 THEN 1 END) as success,
3814
+ count(IF status >= 400 THEN 1 END) as failed,
3815
+ math::mean(duration) as avg_latency
3816
+ FROM requests
3817
+ WHERE timestamp >= $start
3818
+ GROUP ALL
3819
+ `, { start: startTime });
3820
+ } catch (error) {
3821
+ console.error("[Dashboard] Query failed at plugin.ts:180-191", {
3822
+ error,
3823
+ interval,
3824
+ startTime,
3825
+ query: "metrics interval stats",
3826
+ stack: new Error().stack
3827
+ });
3828
+ throw error;
3829
+ }
3830
+ const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
3831
+ return ctx.json({
3832
+ metrics: {
3833
+ totalRequests: s.total || 0,
3834
+ successfulRequests: s.success || 0,
3835
+ failedRequests: s.failed || 0,
3836
+ activeRequests: this.metrics.activeRequests,
3837
+ averageTotalTime_ms: s.avg_latency || 0,
3838
+ recentTimings: this.metrics.recentTimings,
3839
+ logs: [],
3840
+ rateLimitedCounts: this.metrics.rateLimitedCounts,
3841
+ nodeMetrics: this.metrics.nodeMetrics,
3842
+ edgeMetrics: this.metrics.edgeMetrics
3843
+ },
3844
+ uptime
3845
+ });
3846
+ }
3847
+ return ctx.json({
3848
+ metrics: this.metrics,
3849
+ uptime
3850
+ });
3851
+ });
3852
+ this.router.get("/metrics/history", async (ctx) => {
3853
+ const interval = ctx.query["interval"] || "1m";
3854
+ const intervalMap = {
3855
+ "10s": 10 * 1e3,
3856
+ "1m": 60 * 1e3,
3857
+ "5m": 5 * 60 * 1e3,
3858
+ "30m": 30 * 60 * 1e3,
3859
+ "1h": 60 * 60 * 1e3,
3860
+ "2h": 2 * 60 * 60 * 1e3,
3861
+ "6h": 6 * 60 * 60 * 1e3,
3862
+ "12h": 12 * 60 * 60 * 1e3,
3863
+ "1d": 24 * 60 * 60 * 1e3,
3864
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3865
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3866
+ "30d": 30 * 24 * 60 * 60 * 1e3
3867
+ };
3868
+ const periodMs = intervalMap[interval] || 60 * 1e3;
3869
+ const startTime = Date.now() - periodMs * 3;
3870
+ const endTime = Date.now();
3871
+ const result = await datastore.query(
3872
+ "SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
3873
+ { start: startTime, end: endTime, interval }
3874
+ );
3875
+ return ctx.json({
3876
+ metrics: result[0] || []
3082
3877
  });
3083
- const data = await res.json();
3084
- user = {
3085
- id: data.sub,
3086
- name: data.name,
3087
- email: data.email,
3088
- picture: data.picture,
3089
- provider,
3090
- raw: data
3878
+ });
3879
+ const getIntervalStartTime = (interval) => {
3880
+ if (!interval) return 0;
3881
+ const intervalMap = {
3882
+ "10s": 10 * 1e3,
3883
+ "1m": 60 * 1e3,
3884
+ "5m": 5 * 60 * 1e3,
3885
+ "30m": 30 * 60 * 1e3,
3886
+ "1h": 60 * 60 * 1e3,
3887
+ "2h": 2 * 60 * 60 * 1e3,
3888
+ "6h": 6 * 60 * 60 * 1e3,
3889
+ "12h": 12 * 60 * 60 * 1e3,
3890
+ "1d": 24 * 60 * 60 * 1e3,
3891
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3892
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3893
+ "30d": 30 * 24 * 60 * 60 * 1e3
3091
3894
  };
3092
- } else if (provider === "apple") {
3093
- if (idToken) {
3094
- const payload = jose.decodeJwt(idToken);
3095
- user = {
3096
- id: payload.sub,
3097
- email: payload["email"],
3098
- provider,
3099
- raw: payload
3100
- };
3895
+ const ms = intervalMap[interval] || 0;
3896
+ return ms ? Date.now() - ms : 0;
3897
+ };
3898
+ this.router.get("/requests/top", async (ctx) => {
3899
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3900
+ const result = await datastore.query(
3901
+ "SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3902
+ { start: startTime }
3903
+ );
3904
+ return ctx.json({ top: result[0] || [] });
3905
+ });
3906
+ this.router.get("/errors/top", async (ctx) => {
3907
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3908
+ const result = await datastore.query(
3909
+ "SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
3910
+ { start: startTime }
3911
+ );
3912
+ return ctx.json({ top: result[0] || [] });
3913
+ });
3914
+ this.router.get("/requests/failing", async (ctx) => {
3915
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3916
+ const result = await datastore.query(
3917
+ "SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3918
+ { start: startTime }
3919
+ );
3920
+ return ctx.json({ top: result[0] || [] });
3921
+ });
3922
+ this.router.get("/requests/slowest", async (ctx) => {
3923
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3924
+ const result = await datastore.query(
3925
+ "SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
3926
+ { start: startTime }
3927
+ );
3928
+ return ctx.json({ slowest: result[0] || [] });
3929
+ });
3930
+ this.router.get("/registry", (ctx) => {
3931
+ const app = this[$appRoot];
3932
+ if (!this.instrumented && app) {
3933
+ this.instrumentApp(app);
3101
3934
  }
3102
- } else if (provider === "oauth2") {
3103
- if (config.userInfoUrl) {
3104
- const res = await fetch(config.userInfoUrl, {
3105
- headers: { Authorization: `Bearer ${token}` }
3935
+ const registry = app?.getComponentRegistry?.();
3936
+ if (registry) {
3937
+ this.assignIdsToRegistry(registry, "root");
3938
+ }
3939
+ return ctx.json({ registry: registry || {} });
3940
+ });
3941
+ this.router.get("/requests", async (ctx) => {
3942
+ const result = await datastore.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
3943
+ return ctx.json({ requests: result[0] || [] });
3944
+ });
3945
+ this.router.get("/requests/:id", async (ctx) => {
3946
+ const result = await datastore.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
3947
+ return ctx.json({ request: result[0]?.[0] });
3948
+ });
3949
+ this.router.get("/failures", async (ctx) => {
3950
+ const result = await datastore.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
3951
+ return ctx.json({ failures: result[0] });
3952
+ });
3953
+ this.router.post("/replay", async (ctx) => {
3954
+ const body = await ctx.body();
3955
+ const app = this[$appRoot];
3956
+ if (!app) return unknownError(ctx);
3957
+ try {
3958
+ const result = await app.processRequest({
3959
+ method: body.method,
3960
+ path: body.url,
3961
+ // or path
3962
+ headers: body.headers,
3963
+ body: body.body
3106
3964
  });
3107
- const data = await res.json();
3108
- user = {
3109
- id: data.id || data.sub || "unknown",
3110
- name: data.name,
3111
- email: data.email,
3112
- picture: data.picture,
3113
- provider,
3114
- raw: data
3115
- };
3965
+ return ctx.json({
3966
+ status: result.status,
3967
+ headers: result.headers,
3968
+ data: result.data
3969
+ });
3970
+ } catch (e) {
3971
+ return ctx.json({ error: String(e) }, 500);
3116
3972
  }
3973
+ });
3974
+ this.router.get("/", async (ctx) => {
3975
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
3976
+ const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
3977
+ const linkPattern = this.getLinkPattern();
3978
+ const template = await readFile(Dashboard.getBasePath() + "/template.eta", "utf8");
3979
+ return ctx.html(this.eta.renderString(template, {
3980
+ metrics: this.metrics,
3981
+ uptime,
3982
+ rootPath: process.cwd(),
3983
+ linkPattern,
3984
+ headers: this.dashboardConfig.getRequestHeaders?.()
3985
+ }));
3986
+ });
3987
+ }
3988
+ instrumentApp(app) {
3989
+ if (!app.getComponentRegistry) return;
3990
+ const registry = app.getComponentRegistry();
3991
+ this.assignIdsToRegistry(registry, "root");
3992
+ this.instrumented = true;
3993
+ }
3994
+ // Traverses registry, generates IDs, and attaches them to the actual function objects
3995
+ assignIdsToRegistry(node, parentId) {
3996
+ if (!node) return;
3997
+ const makeId = (type, parent, idx, name) => `${type}_${parent}_${idx}_${name.replace(/[^a-zA-Z0-9]/g, "")}`;
3998
+ node.middleware?.forEach((mw, idx) => {
3999
+ const id = makeId("mw", parentId, idx, mw.name);
4000
+ mw.id = id;
4001
+ if (mw._fn) mw._fn._debugId = id;
4002
+ });
4003
+ node.controllers?.forEach((ctrl, idx) => {
4004
+ const id = makeId("ctrl", parentId, idx, ctrl.name);
4005
+ ctrl.id = id;
4006
+ });
4007
+ node.routes?.forEach((r, idx) => {
4008
+ const id = makeId("route", parentId, idx, r.handlerName || "handler");
4009
+ r.id = id;
4010
+ if (r._fn) r._fn._debugId = id;
4011
+ });
4012
+ node.routers?.forEach((r, idx) => {
4013
+ const id = makeId("router", parentId, idx, r.path);
4014
+ r.id = id;
4015
+ this.assignIdsToRegistry(r.children, id);
4016
+ });
4017
+ }
4018
+ recordNodeMetric(id, type, duration, isError) {
4019
+ if (!this.metrics.nodeMetrics[id]) {
4020
+ this.metrics.nodeMetrics[id] = {
4021
+ id,
4022
+ type,
4023
+ requests: 0,
4024
+ totalTime: 0,
4025
+ failures: 0,
4026
+ name: id
4027
+ // simplify
4028
+ };
3117
4029
  }
3118
- return user;
4030
+ const m = this.metrics.nodeMetrics[id];
4031
+ m.requests++;
4032
+ m.totalTime += duration;
4033
+ if (isError) m.failures++;
3119
4034
  }
3120
- /**
3121
- * Middleware to verify JWT
3122
- */
3123
- getMiddleware() {
3124
- return async (ctx, next) => {
3125
- const authHeader = ctx.req.headers.get("Authorization");
3126
- let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
3127
- if (!token) {
3128
- const cookieHeader = ctx.req.headers.get("Cookie");
3129
- token = cookieHeader?.match(/auth_token=([^;]+)/)?.[1] || null;
3130
- }
3131
- if (token) {
4035
+ recordEdgeMetric(from, to) {
4036
+ const key = `${from}|${to}`;
4037
+ this.metrics.edgeMetrics[key] = (this.metrics.edgeMetrics[key] || 0) + 1;
4038
+ }
4039
+ getLinkPattern() {
4040
+ const term = process.env["TERM_PROGRAM"] || "";
4041
+ if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
4042
+ return "vscode://file/{{absolute}}:{{line}}";
4043
+ }
4044
+ return "file:///{{absolute}}:{{line}}";
4045
+ }
4046
+ getHooks() {
4047
+ return {
4048
+ onRequestStart: (ctx) => {
4049
+ const app = this[$appRoot];
4050
+ if (!this.instrumented && app) {
4051
+ this.instrumentApp(app);
4052
+ }
4053
+ this.metrics.totalRequests++;
4054
+ this.metrics.activeRequests++;
4055
+ ctx._debugStartTime = performance.now();
4056
+ ctx[$debug] = new Collector(this);
4057
+ },
4058
+ onResponseEnd: async (ctx, response) => {
4059
+ this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
4060
+ const start = ctx._debugStartTime;
4061
+ let duration = 0;
4062
+ if (start) {
4063
+ duration = performance.now() - start;
4064
+ this.updateTiming(duration);
4065
+ }
4066
+ const isError = response.status >= 400;
4067
+ this.metricsCollector.recordRequest(duration, isError);
4068
+ if (response.status >= 400) {
4069
+ this.metrics.failedRequests++;
4070
+ if (response.status === 429) {
4071
+ const path = ctx.path;
4072
+ this.metrics.rateLimitedCounts[path] = (this.metrics.rateLimitedCounts[path] || 0) + 1;
4073
+ }
4074
+ try {
4075
+ const headers = {};
4076
+ if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
4077
+ ctx.request.headers.forEach((v, k) => {
4078
+ headers[k] = v;
4079
+ });
4080
+ }
4081
+ await datastore.set(new RecordId("failed_requests", ctx.requestId), {
4082
+ method: ctx.method,
4083
+ url: ctx.url.toString(),
4084
+ headers,
4085
+ status: response.status,
4086
+ timestamp: Date.now(),
4087
+ state: ctx.state
4088
+ // body?
4089
+ });
4090
+ } catch (e) {
4091
+ console.error("Failed to record failed request", e);
4092
+ }
4093
+ } else {
4094
+ this.metrics.successfulRequests++;
4095
+ }
4096
+ const logEntry = {
4097
+ method: ctx.method,
4098
+ url: ctx.url.toString(),
4099
+ status: response.status,
4100
+ duration,
4101
+ timestamp: Date.now(),
4102
+ handlerStack: ctx.handlerStack
4103
+ };
4104
+ this.metrics.logs.push(logEntry);
3132
4105
  try {
3133
- const { payload } = await jose.jwtVerify(token, this.secret);
3134
- ctx.user = payload;
3135
- } catch {
4106
+ await datastore.set(new RecordId("requests", ctx.requestId), logEntry);
4107
+ } catch (e) {
4108
+ console.error("Failed to record request log", e);
4109
+ }
4110
+ const retention = this.dashboardConfig.retentionMs ?? 72e5;
4111
+ const cutoff = Date.now() - retention;
4112
+ if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
4113
+ this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
3136
4114
  }
3137
4115
  }
3138
- return next();
3139
4116
  };
3140
4117
  }
4118
+ updateTiming(duration) {
4119
+ const alpha = 0.1;
4120
+ if (this.metrics.averageTotalTime_ms === 0) {
4121
+ this.metrics.averageTotalTime_ms = duration;
4122
+ } else {
4123
+ this.metrics.averageTotalTime_ms = alpha * duration + (1 - alpha) * this.metrics.averageTotalTime_ms;
4124
+ }
4125
+ this.metrics.recentTimings.push(duration);
4126
+ if (this.metrics.recentTimings.length > 50) {
4127
+ this.metrics.recentTimings.shift();
4128
+ }
4129
+ }
4130
+ }
4131
+ function unknownError(ctx) {
4132
+ return ctx.json({ error: "Unknown Error" }, 500);
4133
+ }
4134
+ const eta = new Eta();
4135
+ class ScalarPlugin extends ShokupanRouter {
4136
+ constructor(pluginOptions = {}) {
4137
+ pluginOptions.config ??= {};
4138
+ super();
4139
+ this.pluginOptions = pluginOptions;
4140
+ this.init();
4141
+ }
4142
+ onInit(app, options) {
4143
+ if (options?.path) {
4144
+ app.mount(options.path, this);
4145
+ } else {
4146
+ app.mount(options.path ?? "/", this);
4147
+ }
4148
+ this.onMount(app);
4149
+ }
4150
+ init() {
4151
+ this.get("/", (ctx) => {
4152
+ let path = ctx.url.toString();
4153
+ if (!path.endsWith("/")) path += "/";
4154
+ return ctx.html(eta.renderString(`<!doctype html>
4155
+ <html>
4156
+ <head>
4157
+ <title>API Reference</title>
4158
+ <meta charset = "utf-8" />
4159
+ <meta name="viewport" content = "width=device-width, initial-scale=1" />
4160
+ </head>
4161
+
4162
+ <body>
4163
+ <div id="app"></div>
4164
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
4165
+ <script>
4166
+ Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
4167
+ url: "<%= it.path %>openapi.json",
4168
+ }
4169
+ ])
4170
+ <\/script>
4171
+ </body>
4172
+
4173
+ </html>`, { path, config: this.pluginOptions }));
4174
+ });
4175
+ this.get("/openapi.json", async (ctx) => {
4176
+ let spec;
4177
+ if (this.root.openApiSpec) {
4178
+ try {
4179
+ spec = structuredClone(this.root.openApiSpec);
4180
+ } catch (e) {
4181
+ spec = Object.assign({}, this.root.openApiSpec);
4182
+ }
4183
+ } else {
4184
+ spec = await (this.root || this).generateApiSpec();
4185
+ }
4186
+ if (this.pluginOptions.baseDocument) {
4187
+ deepMerge(spec, this.pluginOptions.baseDocument);
4188
+ }
4189
+ return ctx.json(spec);
4190
+ });
4191
+ }
4192
+ // New lifecycle method to be called by router.mount
4193
+ onMount(parent) {
4194
+ if (parent.onStart) {
4195
+ parent.onStart(async () => {
4196
+ if (this.pluginOptions.enableStaticAnalysis) {
4197
+ try {
4198
+ const entrypoint = process.argv[1];
4199
+ console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
4200
+ const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
4201
+ let staticSpec = await analyzer.analyze();
4202
+ if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
4203
+ deepMerge(this.pluginOptions.baseDocument, staticSpec);
4204
+ console.log("[ScalarPlugin] Static analysis completed successfully.");
4205
+ } catch (err) {
4206
+ console.error("[ScalarPlugin] Failed to run static analysis:", err);
4207
+ }
4208
+ }
4209
+ });
4210
+ }
4211
+ }
3141
4212
  }
3142
4213
  function Compression(options = {}) {
3143
4214
  const threshold = options.threshold ?? 512;
4215
+ const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
3144
4216
  const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
3145
4217
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
3146
4218
  let method = null;
@@ -3153,24 +4225,27 @@ function Compression(options = {}) {
3153
4225
  } else if (acceptEncoding.includes("gzip")) method = "gzip";
3154
4226
  else if (acceptEncoding.includes("deflate")) method = "deflate";
3155
4227
  if (!method) return next();
4228
+ if (!allowedAlgorithms.has(method)) {
4229
+ return next();
4230
+ }
3156
4231
  let response = await next();
3157
- if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
3158
- response = ctx._finalResponse;
4232
+ if (!(response instanceof Response) && ctx[$finalResponse] instanceof Response) {
4233
+ response = ctx[$finalResponse];
3159
4234
  }
3160
4235
  if (response instanceof Response) {
3161
4236
  if (response.headers.has("Content-Encoding")) return response;
3162
4237
  let body;
3163
4238
  let bodySize;
3164
- if (ctx._rawBody !== void 0) {
3165
- if (typeof ctx._rawBody === "string") {
3166
- const encoded = new TextEncoder().encode(ctx._rawBody);
4239
+ if (ctx[$rawBody] !== void 0) {
4240
+ if (typeof ctx[$rawBody] === "string") {
4241
+ const encoded = new TextEncoder().encode(ctx[$rawBody]);
3167
4242
  body = encoded;
3168
4243
  bodySize = encoded.byteLength;
3169
- } else if (ctx._rawBody instanceof Uint8Array) {
3170
- body = ctx._rawBody;
3171
- bodySize = ctx._rawBody.byteLength;
4244
+ } else if (ctx[$rawBody] instanceof Uint8Array) {
4245
+ body = ctx[$rawBody];
4246
+ bodySize = ctx[$rawBody].byteLength;
3172
4247
  } else {
3173
- body = ctx._rawBody;
4248
+ body = ctx[$rawBody];
3174
4249
  bodySize = body.byteLength;
3175
4250
  }
3176
4251
  } else {
@@ -3707,77 +4782,6 @@ function enableOpenApiValidation(app) {
3707
4782
  precompileValidators(app, spec);
3708
4783
  });
3709
4784
  }
3710
- const eta = new Eta();
3711
- class ScalarPlugin extends ShokupanRouter {
3712
- constructor(pluginOptions = {}) {
3713
- pluginOptions.config ??= {};
3714
- super();
3715
- this.pluginOptions = pluginOptions;
3716
- this.init();
3717
- }
3718
- init() {
3719
- this.get("/", (ctx) => {
3720
- let path = ctx.url.toString();
3721
- if (!path.endsWith("/")) path += "/";
3722
- return ctx.html(eta.renderString(`<!doctype html>
3723
- <html>
3724
- <head>
3725
- <title>API Reference</title>
3726
- <meta charset = "utf-8" />
3727
- <meta name="viewport" content = "width=device-width, initial-scale=1" />
3728
- </head>
3729
-
3730
- <body>
3731
- <div id="app"></div>
3732
- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
3733
- <script>
3734
- Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3735
- url: "<%= it.path %>openapi.json",
3736
- }
3737
- ])
3738
- <\/script>
3739
- </body>
3740
-
3741
- </html>`, { path, config: this.pluginOptions }));
3742
- });
3743
- this.get("/openapi.json", async (ctx) => {
3744
- let spec;
3745
- if (this.root.openApiSpec) {
3746
- try {
3747
- spec = structuredClone(this.root.openApiSpec);
3748
- } catch (e) {
3749
- spec = Object.assign({}, this.root.openApiSpec);
3750
- }
3751
- } else {
3752
- spec = await (this.root || this).generateApiSpec();
3753
- }
3754
- if (this.pluginOptions.baseDocument) {
3755
- deepMerge(spec, this.pluginOptions.baseDocument);
3756
- }
3757
- return ctx.json(spec);
3758
- });
3759
- }
3760
- // New lifecycle method to be called by router.mount
3761
- onMount(parent) {
3762
- if (parent.onStart) {
3763
- parent.onStart(async () => {
3764
- if (this.pluginOptions.enableStaticAnalysis) {
3765
- try {
3766
- const entrypoint = process.argv[1];
3767
- console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3768
- const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
3769
- let staticSpec = await analyzer.analyze();
3770
- if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
3771
- deepMerge(this.pluginOptions.baseDocument, staticSpec);
3772
- console.log("[ScalarPlugin] Static analysis completed successfully.");
3773
- } catch (err) {
3774
- console.error("[ScalarPlugin] Failed to run static analysis:", err);
3775
- }
3776
- }
3777
- });
3778
- }
3779
- }
3780
- }
3781
4785
  function SecurityHeaders(options = {}) {
3782
4786
  const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
3783
4787
  const headers = {};
@@ -3901,18 +4905,18 @@ class MemoryStore extends EventEmitter {
3901
4905
  }
3902
4906
  set(sid, sess, cb) {
3903
4907
  this.sessions[sid] = JSON.stringify(sess);
3904
- cb && cb();
4908
+ cb?.();
3905
4909
  }
3906
4910
  destroy(sid, cb) {
3907
4911
  delete this.sessions[sid];
3908
- cb && cb();
4912
+ cb?.();
3909
4913
  }
3910
4914
  touch(sid, sess, cb) {
3911
4915
  const current = this.sessions[sid];
3912
4916
  if (current) {
3913
4917
  this.sessions[sid] = JSON.stringify(sess);
3914
4918
  }
3915
- cb && cb();
4919
+ cb?.();
3916
4920
  }
3917
4921
  all(cb) {
3918
4922
  const result = {};
@@ -3928,7 +4932,7 @@ class MemoryStore extends EventEmitter {
3928
4932
  }
3929
4933
  clear(cb) {
3930
4934
  this.sessions = {};
3931
- cb && cb();
4935
+ cb?.();
3932
4936
  }
3933
4937
  }
3934
4938
  function sign(val, secret) {
@@ -4107,29 +5111,51 @@ function Session(options) {
4107
5111
  }
4108
5112
  export {
4109
5113
  $appRoot,
5114
+ $bodyParseError,
5115
+ $bodyParsed,
5116
+ $bodyType,
5117
+ $cachedBody,
5118
+ $cachedHost,
5119
+ $cachedHostname,
5120
+ $cachedOrigin,
5121
+ $cachedProtocol,
5122
+ $cachedQuery,
4110
5123
  $childControllers,
4111
5124
  $childRouters,
4112
5125
  $controllerPath,
5126
+ $debug,
4113
5127
  $dispatch,
5128
+ $eventMethods,
5129
+ $finalResponse,
5130
+ $io,
4114
5131
  $isApplication,
4115
5132
  $isMounted,
4116
5133
  $isRouter,
4117
5134
  $middleware,
4118
5135
  $mountPath,
4119
5136
  $parent,
5137
+ $rawBody,
5138
+ $requestId,
4120
5139
  $routeArgs,
5140
+ $routeMatched,
4121
5141
  $routeMethods,
4122
5142
  $routeSpec,
4123
5143
  $routes,
5144
+ $socket,
5145
+ $url,
5146
+ $ws,
4124
5147
  All,
4125
5148
  AuthPlugin,
4126
5149
  Body,
5150
+ ClusterPlugin,
4127
5151
  Compression,
4128
5152
  Container,
4129
5153
  Controller,
4130
5154
  Cors,
4131
5155
  Ctx,
5156
+ Dashboard,
4132
5157
  Delete,
5158
+ Event,
4133
5159
  Get,
4134
5160
  HTTPMethods,
4135
5161
  Head,
@@ -4147,16 +5173,13 @@ export {
4147
5173
  RateLimitMiddleware,
4148
5174
  Req,
4149
5175
  RouteParamType,
4150
- RouterRegistry,
4151
5176
  ScalarPlugin,
4152
5177
  SecurityHeaders,
4153
5178
  Session,
4154
5179
  Shokupan,
4155
- ShokupanApplicationTree,
4156
5180
  ShokupanContext,
4157
5181
  ShokupanRequest,
4158
5182
  ShokupanResponse,
4159
- ShokupanRouter,
4160
5183
  Spec,
4161
5184
  Use,
4162
5185
  ValidationError,