shokupan 0.1.0 → 0.3.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 (61) hide show
  1. package/README.md +1 -0
  2. package/dist/benchmarking/advanced-cases/elysia.d.ts +1 -0
  3. package/dist/benchmarking/advanced-cases/express.d.ts +1 -0
  4. package/dist/benchmarking/advanced-cases/fastify.d.ts +1 -0
  5. package/dist/benchmarking/advanced-cases/hapi.d.ts +1 -0
  6. package/dist/benchmarking/advanced-cases/hono.d.ts +1 -0
  7. package/dist/benchmarking/advanced-cases/koa.d.ts +1 -0
  8. package/dist/benchmarking/advanced-cases/nest.d.ts +1 -0
  9. package/dist/benchmarking/advanced-cases/shokupan.d.ts +1 -0
  10. package/dist/benchmarking/advanced-data.d.ts +33 -0
  11. package/dist/benchmarking/advanced-runner.d.ts +1 -0
  12. package/dist/benchmarking/advanced-worker.d.ts +0 -0
  13. package/dist/benchmarking/cases/elysia.d.ts +1 -0
  14. package/dist/benchmarking/cases/express.d.ts +1 -0
  15. package/dist/benchmarking/cases/fastify.d.ts +1 -0
  16. package/dist/benchmarking/cases/hapi.d.ts +1 -0
  17. package/dist/benchmarking/cases/hono.d.ts +1 -0
  18. package/dist/benchmarking/cases/koa.d.ts +1 -0
  19. package/dist/benchmarking/cases/nest.d.ts +1 -0
  20. package/dist/benchmarking/cases/shokupan.d.ts +1 -0
  21. package/dist/benchmarking/data.d.ts +15 -0
  22. package/dist/benchmarking/quick_bench.d.ts +1 -0
  23. package/dist/benchmarking/runner.d.ts +1 -0
  24. package/dist/benchmarking/worker.d.ts +0 -0
  25. package/dist/buntest.d.ts +1 -0
  26. package/dist/cli.cjs +1 -1
  27. package/dist/cli.js +1 -1
  28. package/dist/context.d.ts +25 -8
  29. package/dist/decorators.d.ts +47 -0
  30. package/dist/index.cjs +1538 -655
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1532 -651
  34. package/dist/index.js.map +1 -1
  35. package/dist/middleware.d.ts +2 -0
  36. package/dist/{openapi-analyzer-cjdGeQ5a.js → openapi-analyzer-BtIaHIfe.js} +14 -6
  37. package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
  38. package/dist/{openapi-analyzer-CFqgSLNK.cjs → openapi-analyzer-D9YB3IkV.cjs} +14 -6
  39. package/dist/openapi-analyzer-D9YB3IkV.cjs.map +1 -0
  40. package/dist/plugins/auth.d.ts +1 -1
  41. package/dist/plugins/debugview/plugin.d.ts +28 -0
  42. package/dist/plugins/failed-request-recorder.d.ts +14 -0
  43. package/dist/plugins/idempotency/plugin.d.ts +14 -0
  44. package/dist/plugins/openapi-validator.d.ts +30 -0
  45. package/dist/plugins/proxy.d.ts +9 -0
  46. package/dist/plugins/rate-limit.d.ts +3 -1
  47. package/dist/plugins/serve-static.d.ts +2 -3
  48. package/dist/response.d.ts +4 -0
  49. package/dist/router/trie.d.ts +14 -0
  50. package/dist/router.d.ts +50 -3
  51. package/dist/server-adapter-BWrEJbKL.js +64 -0
  52. package/dist/server-adapter-BWrEJbKL.js.map +1 -0
  53. package/dist/server-adapter-fVKP60e0.cjs +81 -0
  54. package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
  55. package/dist/shokupan.d.ts +16 -3
  56. package/dist/types.d.ts +108 -4
  57. package/dist/util/cpu-monitor.d.ts +11 -0
  58. package/dist/util/stack.d.ts +8 -0
  59. package/package.json +8 -3
  60. package/dist/openapi-analyzer-CFqgSLNK.cjs.map +0 -1
  61. package/dist/openapi-analyzer-cjdGeQ5a.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,17 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const api = require("@opentelemetry/api");
3
+ const promises = require("node:fs/promises");
4
4
  const eta$2 = require("eta");
5
- const promises = require("fs/promises");
5
+ const promises$1 = require("fs/promises");
6
6
  const path = require("path");
7
7
  const node_async_hooks = require("node:async_hooks");
8
+ const node = require("@surrealdb/node");
9
+ const surrealdb = require("surrealdb");
10
+ const api = require("@opentelemetry/api");
11
+ const os = require("node:os");
8
12
  const arctic = require("arctic");
9
13
  const jose = require("jose");
10
- const openapiAnalyzer = require("./openapi-analyzer-CFqgSLNK.cjs");
11
- const crypto = require("crypto");
12
- const events = require("events");
14
+ const zlib = require("node:zlib");
15
+ const Ajv = require("ajv");
16
+ const addFormats = require("ajv-formats");
13
17
  const classTransformer = require("class-transformer");
14
18
  const classValidator = require("class-validator");
19
+ const openapiAnalyzer = require("./openapi-analyzer-D9YB3IkV.cjs");
20
+ const crypto = require("crypto");
21
+ const events = require("events");
15
22
  function _interopNamespaceDefault(e) {
16
23
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
17
24
  if (e) {
@@ -28,14 +35,17 @@ function _interopNamespaceDefault(e) {
28
35
  n.default = e;
29
36
  return Object.freeze(n);
30
37
  }
38
+ const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
31
39
  const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
40
+ const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
32
41
  class ShokupanResponse {
33
- _headers = new Headers();
42
+ _headers = null;
34
43
  _status = 200;
35
44
  /**
36
45
  * Get the current headers
37
46
  */
38
47
  get headers() {
48
+ if (!this._headers) this._headers = new Headers();
39
49
  return this._headers;
40
50
  }
41
51
  /**
@@ -56,6 +66,7 @@ class ShokupanResponse {
56
66
  * @param value Header value
57
67
  */
58
68
  set(key, value) {
69
+ if (!this._headers) this._headers = new Headers();
59
70
  this._headers.set(key, value);
60
71
  return this;
61
72
  }
@@ -65,6 +76,7 @@ class ShokupanResponse {
65
76
  * @param value Header value
66
77
  */
67
78
  append(key, value) {
79
+ if (!this._headers) this._headers = new Headers();
68
80
  this._headers.append(key, value);
69
81
  return this;
70
82
  }
@@ -73,29 +85,62 @@ class ShokupanResponse {
73
85
  * @param key Header name
74
86
  */
75
87
  get(key) {
76
- return this._headers.get(key);
88
+ return this._headers?.get(key) || null;
77
89
  }
78
90
  /**
79
91
  * Check if a header exists
80
92
  * @param key Header name
81
93
  */
82
94
  has(key) {
83
- return this._headers.has(key);
95
+ return this._headers?.has(key) || false;
96
+ }
97
+ /**
98
+ * Internal: check if headers have been initialized/modified
99
+ */
100
+ get hasPopulatedHeaders() {
101
+ return this._headers !== null;
84
102
  }
85
103
  }
86
104
  class ShokupanContext {
87
- constructor(request, server, state, app) {
105
+ // Raw body for compression optimization
106
+ constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
88
107
  this.request = request;
89
108
  this.server = server;
90
109
  this.app = app;
91
- this.url = new URL(request.url);
110
+ this.signal = signal;
92
111
  this.state = state || {};
112
+ if (enableMiddlewareTracking) {
113
+ const self = this;
114
+ this.state = new Proxy(this.state, {
115
+ set(target, p, newValue, receiver) {
116
+ const result = Reflect.set(target, p, newValue, receiver);
117
+ const currentHandler = self.handlerStack[self.handlerStack.length - 1];
118
+ if (currentHandler) {
119
+ if (!currentHandler.stateChanges) currentHandler.stateChanges = {};
120
+ currentHandler.stateChanges[p] = newValue;
121
+ }
122
+ return result;
123
+ }
124
+ });
125
+ }
93
126
  this.response = new ShokupanResponse();
94
127
  }
95
- url;
128
+ _url;
96
129
  params = {};
130
+ // Router assigns this, but default to empty object
97
131
  state;
132
+ handlerStack = [];
98
133
  response;
134
+ _debug;
135
+ _finalResponse;
136
+ _rawBody;
137
+ get url() {
138
+ if (!this._url) {
139
+ const urlString = this.request.url || "http://localhost/";
140
+ this._url = new URL(urlString);
141
+ }
142
+ return this._url;
143
+ }
99
144
  /**
100
145
  * Base request
101
146
  */
@@ -112,13 +157,42 @@ class ShokupanContext {
112
157
  * Request path
113
158
  */
114
159
  get path() {
115
- return this.url.pathname;
160
+ if (this._url) return this._url.pathname;
161
+ const url = this.request.url;
162
+ let queryIndex = url.indexOf("?");
163
+ const end = queryIndex === -1 ? url.length : queryIndex;
164
+ let start = 0;
165
+ const protocolIndex = url.indexOf("://");
166
+ if (protocolIndex !== -1) {
167
+ const hostStart = protocolIndex + 3;
168
+ const pathStart = url.indexOf("/", hostStart);
169
+ if (pathStart !== -1 && pathStart < end) {
170
+ start = pathStart;
171
+ } else {
172
+ return "/";
173
+ }
174
+ } else {
175
+ if (url.charCodeAt(0) === 47) {
176
+ start = 0;
177
+ }
178
+ }
179
+ return url.substring(start, end);
116
180
  }
117
181
  /**
118
182
  * Request query params
119
183
  */
120
184
  get query() {
121
- return Object.fromEntries(this.url.searchParams);
185
+ const q = {};
186
+ for (const [key, value] of this.url.searchParams) {
187
+ if (q[key] === void 0) {
188
+ q[key] = value;
189
+ } else if (Array.isArray(q[key])) {
190
+ q[key].push(value);
191
+ } else {
192
+ q[key] = [q[key], value];
193
+ }
194
+ }
195
+ return q;
122
196
  }
123
197
  /**
124
198
  * Client IP address
@@ -193,25 +267,60 @@ class ShokupanContext {
193
267
  setCookie(name, value, options = {}) {
194
268
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
195
269
  if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
270
+ if (options.domain) cookie += `; Domain=${options.domain}`;
271
+ if (options.path) cookie += `; Path=${options.path || "/"}`;
196
272
  if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
197
273
  if (options.httpOnly) cookie += `; HttpOnly`;
198
274
  if (options.secure) cookie += `; Secure`;
199
- if (options.domain) cookie += `; Domain=${options.domain}`;
200
- if (options.path) cookie += `; Path=${options.path || "/"}`;
201
- if (options.sameSite) {
202
- typeof options.sameSite === "string" ? options.sameSite.toLowerCase() : options.sameSite ? "strict" : "lax";
203
- cookie += `; SameSite=${typeof options.sameSite === "boolean" ? "Strict" : options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1)}`;
275
+ let sameSite = options.sameSite;
276
+ if (sameSite === true) sameSite = "Strict";
277
+ if (sameSite === void 0 || sameSite === false) ;
278
+ else {
279
+ const stringSameSite = typeof sameSite === "string" ? sameSite.toLowerCase() : sameSite;
280
+ switch (stringSameSite) {
281
+ case "lax":
282
+ cookie += "; SameSite=Lax";
283
+ break;
284
+ case "strict":
285
+ cookie += "; SameSite=Strict";
286
+ break;
287
+ case "none":
288
+ cookie += "; SameSite=None";
289
+ break;
290
+ default:
291
+ cookie += "; SameSite=Lax";
292
+ break;
293
+ }
204
294
  }
205
295
  if (options.priority) {
206
- cookie += `; Priority=${options.priority.charAt(0).toUpperCase() + options.priority.slice(1)}`;
296
+ const p = options.priority.toLowerCase();
297
+ if (p === "low") cookie += "; Priority=Low";
298
+ else if (p === "medium") cookie += "; Priority=Medium";
299
+ else if (p === "high") cookie += "; Priority=High";
207
300
  }
208
301
  this.response.append("Set-Cookie", cookie);
209
302
  return this;
210
303
  }
211
304
  mergeHeaders(headers) {
212
- const h = new Headers(this.response.headers);
305
+ let h;
306
+ if (this.response.hasPopulatedHeaders) {
307
+ h = new Headers(this.response.headers);
308
+ } else {
309
+ h = new Headers();
310
+ }
213
311
  if (headers) {
214
- new Headers(headers).forEach((v, k) => h.set(k, v));
312
+ if (headers instanceof Headers) {
313
+ headers.forEach((v, k) => h.set(k, v));
314
+ } else if (Array.isArray(headers)) {
315
+ headers.forEach(([k, v]) => h.set(k, v));
316
+ } else {
317
+ const keys = Object.keys(headers);
318
+ for (let i = 0; i < keys.length; i++) {
319
+ const key = keys[i];
320
+ const val = headers[key];
321
+ h.set(key, val);
322
+ }
323
+ }
215
324
  }
216
325
  return h;
217
326
  }
@@ -224,17 +333,21 @@ class ShokupanContext {
224
333
  send(body, options) {
225
334
  const headers = this.mergeHeaders(options?.headers);
226
335
  const status = options?.status ?? this.response.status;
227
- return new Response(body, { status, headers });
336
+ if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
337
+ this._rawBody = body;
338
+ }
339
+ this._finalResponse = new Response(body, { status, headers });
340
+ return this._finalResponse;
228
341
  }
229
342
  /**
230
343
  * Read request body
231
344
  */
232
345
  async body() {
233
- const contentType = this.request.headers.get("content-type");
234
- if (contentType?.includes("application/json")) {
346
+ const contentType = this.request.headers.get("content-type") || "";
347
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
235
348
  return this.request.json();
236
349
  }
237
- if (contentType?.includes("multipart/form-data") || contentType?.includes("application/x-www-form-urlencoded")) {
350
+ if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
238
351
  return this.request.formData();
239
352
  }
240
353
  return this.request.text();
@@ -243,28 +356,49 @@ class ShokupanContext {
243
356
  * Respond with a JSON object
244
357
  */
245
358
  json(data, status, headers) {
359
+ const finalStatus = status ?? this.response.status;
360
+ const jsonString = JSON.stringify(data);
361
+ this._rawBody = jsonString;
362
+ if (!headers && !this.response.hasPopulatedHeaders) {
363
+ this._finalResponse = new Response(jsonString, {
364
+ status: finalStatus,
365
+ headers: { "content-type": "application/json" }
366
+ });
367
+ return this._finalResponse;
368
+ }
246
369
  const finalHeaders = this.mergeHeaders(headers);
247
370
  finalHeaders.set("content-type", "application/json");
248
- const finalStatus = status ?? this.response.status;
249
- return new Response(JSON.stringify(data), { status: finalStatus, headers: finalHeaders });
371
+ this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
372
+ return this._finalResponse;
250
373
  }
251
374
  /**
252
375
  * Respond with a text string
253
376
  */
254
377
  text(data, status, headers) {
255
- const finalHeaders = this.mergeHeaders(headers);
256
- finalHeaders.set("content-type", "text/plain");
257
378
  const finalStatus = status ?? this.response.status;
258
- return new Response(data, { status: finalStatus, headers: finalHeaders });
379
+ this._rawBody = data;
380
+ if (!headers && !this.response.hasPopulatedHeaders) {
381
+ this._finalResponse = new Response(data, {
382
+ status: finalStatus,
383
+ headers: { "content-type": "text/plain; charset=utf-8" }
384
+ });
385
+ return this._finalResponse;
386
+ }
387
+ const finalHeaders = this.mergeHeaders(headers);
388
+ finalHeaders.set("content-type", "text/plain; charset=utf-8");
389
+ this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
390
+ return this._finalResponse;
259
391
  }
260
392
  /**
261
393
  * Respond with HTML content
262
394
  */
263
395
  html(html, status, headers) {
264
- const finalHeaders = this.mergeHeaders(headers);
265
- finalHeaders.set("content-type", "text/html");
266
396
  const finalStatus = status ?? this.response.status;
267
- return new Response(html, { status: finalStatus, headers: finalHeaders });
397
+ const finalHeaders = this.mergeHeaders(headers);
398
+ finalHeaders.set("content-type", "text/html; charset=utf-8");
399
+ this._rawBody = html;
400
+ this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
401
+ return this._finalResponse;
268
402
  }
269
403
  /**
270
404
  * Respond with a redirect
@@ -272,7 +406,8 @@ class ShokupanContext {
272
406
  redirect(url, status = 302) {
273
407
  const headers = this.mergeHeaders();
274
408
  headers.set("Location", url);
275
- return new Response(null, { status, headers });
409
+ this._finalResponse = new Response(null, { status, headers });
410
+ return this._finalResponse;
276
411
  }
277
412
  /**
278
413
  * Respond with a status code
@@ -280,15 +415,26 @@ class ShokupanContext {
280
415
  */
281
416
  status(status) {
282
417
  const headers = this.mergeHeaders();
283
- return new Response(null, { status, headers });
418
+ this._finalResponse = new Response(null, { status, headers });
419
+ return this._finalResponse;
284
420
  }
285
421
  /**
286
422
  * Respond with a file
287
423
  */
288
- file(path2, fileOptions, responseOptions) {
424
+ async file(path2, fileOptions, responseOptions) {
289
425
  const headers = this.mergeHeaders(responseOptions?.headers);
290
426
  const status = responseOptions?.status ?? this.response.status;
291
- return new Response(Bun.file(path2, fileOptions), { status, headers });
427
+ if (typeof Bun !== "undefined") {
428
+ this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
429
+ return this._finalResponse;
430
+ } else {
431
+ const fileBuffer = await promises.readFile(path2);
432
+ if (fileOptions?.type) {
433
+ headers.set("content-type", fileOptions.type);
434
+ }
435
+ this._finalResponse = new Response(fileBuffer, { status, headers });
436
+ return this._finalResponse;
437
+ }
292
438
  }
293
439
  /**
294
440
  * JSX Rendering Function
@@ -308,6 +454,74 @@ class ShokupanContext {
308
454
  return this.html(html, status, headers);
309
455
  }
310
456
  }
457
+ function RateLimitMiddleware(options = {}) {
458
+ const windowMs = options.windowMs || 60 * 1e3;
459
+ const max = options.limit || options.max || 5;
460
+ const message = options.message || "Too many requests, please try again later.";
461
+ const statusCode = options.statusCode || 429;
462
+ const headers = options.headers !== false;
463
+ const mode = options.mode || "user";
464
+ const keyGenerator = options.keyGenerator || ((ctx) => {
465
+ if (mode === "absolute") {
466
+ return "global";
467
+ }
468
+ return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
469
+ });
470
+ const skip = options.skip || (() => false);
471
+ const hits = /* @__PURE__ */ new Map();
472
+ const interval = setInterval(() => {
473
+ const now = Date.now();
474
+ for (const [key, record] of hits.entries()) {
475
+ if (record.resetTime <= now) {
476
+ hits.delete(key);
477
+ }
478
+ }
479
+ }, windowMs);
480
+ if (interval.unref) interval.unref();
481
+ const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
482
+ if (skip(ctx)) return next();
483
+ const key = keyGenerator(ctx);
484
+ const now = Date.now();
485
+ let record = hits.get(key);
486
+ if (!record || record.resetTime <= now) {
487
+ record = {
488
+ hits: 0,
489
+ resetTime: now + windowMs
490
+ };
491
+ hits.set(key, record);
492
+ }
493
+ record.hits++;
494
+ const remaining = Math.max(0, max - record.hits);
495
+ const resetTime = Math.ceil(record.resetTime / 1e3);
496
+ const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
497
+ const setHeaders = (res) => {
498
+ if (!headers || !res || !res.headers) return;
499
+ try {
500
+ res.headers.set("X-RateLimit-Limit", String(max));
501
+ res.headers.set("X-RateLimit-Remaining", String(remaining));
502
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
503
+ } catch (e) {
504
+ }
505
+ };
506
+ if (record.hits > max) {
507
+ typeof message === "object" ? JSON.stringify(message) : String(message);
508
+ const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
509
+ if (headers) {
510
+ setHeaders(res);
511
+ res.headers.set("Retry-After", String(retryAfter));
512
+ }
513
+ return res;
514
+ }
515
+ const response = await next();
516
+ if (response instanceof Response && headers) {
517
+ setHeaders(response);
518
+ }
519
+ return response;
520
+ };
521
+ rateLimitMiddleware.isBuiltin = true;
522
+ rateLimitMiddleware.pluginName = "RateLimit";
523
+ return rateLimitMiddleware;
524
+ }
311
525
  const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
312
526
  const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
313
527
  const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
@@ -404,6 +618,9 @@ const Patch = createMethodDecorator("PATCH");
404
618
  const Options = createMethodDecorator("OPTIONS");
405
619
  const Head = createMethodDecorator("HEAD");
406
620
  const All = createMethodDecorator("ALL");
621
+ function RateLimit(options) {
622
+ return Use(RateLimitMiddleware(options));
623
+ }
407
624
  class Container {
408
625
  static services = /* @__PURE__ */ new Map();
409
626
  static register(target, instance) {
@@ -437,69 +654,43 @@ function Inject(token) {
437
654
  });
438
655
  };
439
656
  }
440
- const tracer = api.trace.getTracer("shokupan.middleware");
441
- function traceMiddleware(fn, name) {
442
- const middlewareName = fn.name || "anonymous middleware";
443
- return async (ctx, next) => {
444
- return tracer.startActiveSpan(`middleware - ${middlewareName}`, {
445
- kind: api.SpanKind.INTERNAL,
446
- attributes: {
447
- "code.function": middlewareName,
448
- "component": "shokupan.middleware"
449
- }
450
- }, async (span) => {
451
- try {
452
- const result = await fn(ctx, next);
453
- return result;
454
- } catch (err) {
455
- span.recordException(err);
456
- span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
457
- throw err;
458
- } finally {
459
- span.end();
460
- }
461
- });
462
- };
463
- }
464
- function traceHandler(fn, name) {
465
- return async function(...args) {
466
- return tracer.startActiveSpan(`route handler - ${name}`, {
467
- kind: api.SpanKind.INTERNAL,
468
- attributes: {
469
- "http.route": name,
470
- "component": "shokupan.route"
471
- }
472
- }, async (span) => {
657
+ const compose = (middleware) => {
658
+ if (!middleware.length) {
659
+ return (context, next) => {
660
+ return next ? next() : Promise.resolve();
661
+ };
662
+ }
663
+ return function dispatch(context, next) {
664
+ let index = -1;
665
+ async function runner(i) {
666
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
667
+ index = i;
668
+ if (i >= middleware.length) {
669
+ return next ? next() : Promise.resolve();
670
+ }
671
+ const fn = middleware[i];
672
+ if (!context._debug) {
673
+ return fn(context, () => runner(i + 1));
674
+ }
675
+ const debug = context._debug;
676
+ const debugId = fn._debugId || fn.name || "anonymous";
677
+ const previousNode = debug.getCurrentNode();
678
+ debug.trackEdge(previousNode, debugId);
679
+ debug.setNode(debugId);
680
+ const start = performance.now();
473
681
  try {
474
- const result = await fn.apply(this, args);
475
- return result;
682
+ const res = await Promise.resolve(fn(context, () => runner(i + 1)));
683
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
684
+ return res;
476
685
  } catch (err) {
477
- span.recordException(err);
478
- span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
479
- throw err;
686
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
687
+ return Promise.reject(err);
480
688
  } finally {
481
- span.end();
689
+ if (previousNode) debug.setNode(previousNode);
482
690
  }
483
- });
484
- };
485
- }
486
- const compose = (middleware) => {
487
- function fn(context, next) {
488
- let runner = next || (async () => {
489
- });
490
- for (let i = middleware.length - 1; i >= 0; i--) {
491
- const fn2 = traceMiddleware(middleware[i]);
492
- const nextStep = runner;
493
- let called = false;
494
- runner = async () => {
495
- if (called) throw new Error("next() called multiple times");
496
- called = true;
497
- return fn2(context, nextStep);
498
- };
499
691
  }
500
- return runner();
501
- }
502
- return fn;
692
+ return runner(0);
693
+ };
503
694
  };
504
695
  class ShokupanRequestBase {
505
696
  method;
@@ -557,6 +748,15 @@ function deepMerge(target, ...sources) {
557
748
  }
558
749
  return deepMerge(target, ...sources);
559
750
  }
751
+ const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
752
+ const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
753
+ const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
754
+ const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
755
+ const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
756
+ const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
757
+ const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
758
+ const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
759
+ const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
560
760
  function analyzeHandler(handler) {
561
761
  const handlerSource = handler.toString();
562
762
  const inferredSpec = {};
@@ -566,46 +766,28 @@ function analyzeHandler(handler) {
566
766
  };
567
767
  }
568
768
  const queryParams = /* @__PURE__ */ new Map();
569
- const queryIntMatch = handlerSource.match(/parseInt\(ctx\.query\.(\w+)\)/g);
570
- if (queryIntMatch) {
571
- queryIntMatch.forEach((match) => {
572
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
573
- if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
574
- });
769
+ for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
770
+ if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
575
771
  }
576
- const queryFloatMatch = handlerSource.match(/parseFloat\(ctx\.query\.(\w+)\)/g);
577
- if (queryFloatMatch) {
578
- queryFloatMatch.forEach((match) => {
579
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
580
- if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
581
- });
772
+ for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
773
+ if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
582
774
  }
583
- const queryNumberMatch = handlerSource.match(/Number\(ctx\.query\.(\w+)\)/g);
584
- if (queryNumberMatch) {
585
- queryNumberMatch.forEach((match) => {
586
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
587
- if (paramName && !queryParams.has(paramName)) {
588
- queryParams.set(paramName, { type: "number" });
589
- }
590
- });
775
+ for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
776
+ if (match[1] && !queryParams.has(match[1])) {
777
+ queryParams.set(match[1], { type: "number" });
778
+ }
591
779
  }
592
- const queryBoolMatch = handlerSource.match(/(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g);
593
- if (queryBoolMatch) {
594
- queryBoolMatch.forEach((match) => {
595
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
596
- if (paramName && !queryParams.has(paramName)) {
597
- queryParams.set(paramName, { type: "boolean" });
598
- }
599
- });
780
+ for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
781
+ const name = match[1] || match[2];
782
+ if (name && !queryParams.has(name)) {
783
+ queryParams.set(name, { type: "boolean" });
784
+ }
600
785
  }
601
- const queryMatch = handlerSource.match(/ctx\.query\.(\w+)/g);
602
- if (queryMatch) {
603
- queryMatch.forEach((match) => {
604
- const paramName = match.split(".")[2];
605
- if (paramName && !queryParams.has(paramName)) {
606
- queryParams.set(paramName, { type: "string" });
607
- }
608
- });
786
+ for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
787
+ const name = match[1];
788
+ if (name && !queryParams.has(name)) {
789
+ queryParams.set(name, { type: "string" });
790
+ }
609
791
  }
610
792
  if (queryParams.size > 0) {
611
793
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -618,19 +800,11 @@ function analyzeHandler(handler) {
618
800
  });
619
801
  }
620
802
  const pathParams = /* @__PURE__ */ new Map();
621
- const paramIntMatch = handlerSource.match(/parseInt\(ctx\.params\.(\w+)\)/g);
622
- if (paramIntMatch) {
623
- paramIntMatch.forEach((match) => {
624
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
625
- if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
626
- });
803
+ for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
804
+ if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
627
805
  }
628
- const paramFloatMatch = handlerSource.match(/parseFloat\(ctx\.params\.(\w+)\)/g);
629
- if (paramFloatMatch) {
630
- paramFloatMatch.forEach((match) => {
631
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
632
- if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
633
- });
806
+ for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
807
+ if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
634
808
  }
635
809
  if (pathParams.size > 0) {
636
810
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -643,76 +817,55 @@ function analyzeHandler(handler) {
643
817
  });
644
818
  });
645
819
  }
646
- const headerMatch = handlerSource.match(/ctx\.get\(['"](\w+)['"]\)/g);
647
- if (headerMatch) {
648
- if (!inferredSpec.parameters) inferredSpec.parameters = [];
649
- headerMatch.forEach((match) => {
650
- const headerName = match.match(/['"](\w+)['"]/)?.[1];
651
- if (headerName) {
652
- inferredSpec.parameters.push({
653
- name: headerName,
654
- in: "header",
655
- schema: { type: "string" }
656
- });
657
- }
658
- });
820
+ for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
821
+ if (match[1]) {
822
+ if (!inferredSpec.parameters) inferredSpec.parameters = [];
823
+ inferredSpec.parameters.push({
824
+ name: match[1],
825
+ in: "header",
826
+ schema: { type: "string" }
827
+ });
828
+ }
659
829
  }
660
830
  const responses = {};
661
831
  if (handlerSource.includes("ctx.json(")) {
662
832
  responses["200"] = {
663
833
  description: "Successful response",
664
- content: {
665
- "application/json": { schema: { type: "object" } }
666
- }
834
+ content: { "application/json": { schema: { type: "object" } } }
667
835
  };
668
836
  }
669
837
  if (handlerSource.includes("ctx.html(")) {
670
838
  responses["200"] = {
671
839
  description: "Successful response",
672
- content: {
673
- "text/html": { schema: { type: "string" } }
674
- }
840
+ content: { "text/html": { schema: { type: "string" } } }
675
841
  };
676
842
  }
677
843
  if (handlerSource.includes("ctx.text(")) {
678
844
  responses["200"] = {
679
845
  description: "Successful response",
680
- content: {
681
- "text/plain": { schema: { type: "string" } }
682
- }
846
+ content: { "text/plain": { schema: { type: "string" } } }
683
847
  };
684
848
  }
685
849
  if (handlerSource.includes("ctx.file(")) {
686
850
  responses["200"] = {
687
851
  description: "File download",
688
- content: {
689
- "application/octet-stream": { schema: { type: "string", format: "binary" } }
690
- }
852
+ content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
691
853
  };
692
854
  }
693
855
  if (handlerSource.includes("ctx.redirect(")) {
694
- responses["302"] = {
695
- description: "Redirect"
696
- };
856
+ responses["302"] = { description: "Redirect" };
697
857
  }
698
858
  if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
699
859
  responses["200"] = {
700
860
  description: "Successful response",
701
- content: {
702
- "application/json": { schema: { type: "object" } }
703
- }
861
+ content: { "application/json": { schema: { type: "object" } } }
704
862
  };
705
863
  }
706
- const errorStatusMatch = handlerSource.match(/ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g);
707
- if (errorStatusMatch) {
708
- errorStatusMatch.forEach((match) => {
709
- const statusCode = match.match(/,\s*(\d{3,})\)/)?.[1];
710
- if (statusCode && statusCode !== "200") {
711
- responses[statusCode] = {
712
- description: `Error response (${statusCode})`
713
- };
714
- }
715
- });
864
+ for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
865
+ const statusCode = match[1];
866
+ if (statusCode && statusCode !== "200") {
867
+ responses[statusCode] = { description: `Error response (${statusCode})` };
868
+ }
716
869
  }
717
870
  if (Object.keys(responses).length > 0) {
718
871
  inferredSpec.responses = responses;
@@ -726,7 +879,7 @@ async function generateOpenApi(rootRouter, options = {}) {
726
879
  const defaultTagName = options.defaultTag || "Application";
727
880
  let astRoutes = [];
728
881
  try {
729
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-CFqgSLNK.cjs"));
882
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-D9YB3IkV.cjs"));
730
883
  const analyzer = new OpenAPIAnalyzer(process.cwd());
731
884
  const { applications } = await analyzer.analyze();
732
885
  const appMap = /* @__PURE__ */ new Map();
@@ -996,10 +1149,10 @@ async function generateOpenApi(rootRouter, options = {}) {
996
1149
  };
997
1150
  }
998
1151
  const eta$1 = new eta$2.Eta();
999
- function serveStatic(ctx, config, prefix) {
1152
+ function serveStatic(config, prefix) {
1000
1153
  const rootPath = path.resolve(config.root || ".");
1001
1154
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1002
- return async () => {
1155
+ const serveStaticMiddleware = async (ctx) => {
1003
1156
  let relative = ctx.path.slice(normalizedPrefix.length);
1004
1157
  if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
1005
1158
  if (relative.length === 0) relative = "/";
@@ -1032,13 +1185,13 @@ function serveStatic(ctx, config, prefix) {
1032
1185
  let finalPath = requestPath;
1033
1186
  let stats;
1034
1187
  try {
1035
- stats = await promises.stat(requestPath);
1188
+ stats = await promises$1.stat(requestPath);
1036
1189
  } catch (e) {
1037
1190
  if (config.extensions) {
1038
1191
  for (const ext of config.extensions) {
1039
1192
  const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
1040
1193
  try {
1041
- const s = await promises.stat(p);
1194
+ const s = await promises$1.stat(p);
1042
1195
  if (s.isFile()) {
1043
1196
  finalPath = p;
1044
1197
  stats = s;
@@ -1067,7 +1220,7 @@ function serveStatic(ctx, config, prefix) {
1067
1220
  for (const idx of indexes) {
1068
1221
  const idxPath = path.join(finalPath, idx);
1069
1222
  try {
1070
- const idxStats = await promises.stat(idxPath);
1223
+ const idxStats = await promises$1.stat(idxPath);
1071
1224
  if (idxStats.isFile()) {
1072
1225
  finalPath = idxPath;
1073
1226
  foundIndex = true;
@@ -1079,7 +1232,7 @@ function serveStatic(ctx, config, prefix) {
1079
1232
  if (!foundIndex) {
1080
1233
  if (config.listDirectory) {
1081
1234
  try {
1082
- const files = await promises.readdir(requestPath);
1235
+ const files = await promises$1.readdir(requestPath);
1083
1236
  const listing = eta$1.renderString(`
1084
1237
  <!DOCTYPE html>
1085
1238
  <html>
@@ -1116,16 +1269,209 @@ function serveStatic(ctx, config, prefix) {
1116
1269
  }
1117
1270
  }
1118
1271
  }
1119
- const file = Bun.file(finalPath);
1120
- let response = new Response(file);
1272
+ let response;
1273
+ if (typeof Bun !== "undefined") {
1274
+ response = new Response(Bun.file(finalPath));
1275
+ } else {
1276
+ const fileBuffer = await promises$1.readFile(finalPath);
1277
+ response = new Response(fileBuffer);
1278
+ }
1121
1279
  if (config.hooks?.onResponse) {
1122
1280
  const hooked = await config.hooks.onResponse(ctx, response);
1123
1281
  if (hooked) response = hooked;
1124
1282
  }
1125
1283
  return response;
1126
1284
  };
1285
+ serveStaticMiddleware.isBuiltin = true;
1286
+ serveStaticMiddleware.pluginName = "ServeStatic";
1287
+ return serveStaticMiddleware;
1288
+ }
1289
+ class RouterTrie {
1290
+ root;
1291
+ constructor() {
1292
+ this.root = this.createNode();
1293
+ }
1294
+ createNode() {
1295
+ return {
1296
+ children: {}
1297
+ };
1298
+ }
1299
+ insert(method, path2, handler) {
1300
+ let node2 = this.root;
1301
+ const segments = this.splitPath(path2);
1302
+ for (const segment of segments) {
1303
+ if (segment === "**") {
1304
+ if (!node2.recursiveChild) {
1305
+ node2.recursiveChild = this.createNode();
1306
+ }
1307
+ node2 = node2.recursiveChild;
1308
+ } else if (segment === "*") {
1309
+ if (!node2.wildcardChild) {
1310
+ node2.wildcardChild = this.createNode();
1311
+ }
1312
+ node2 = node2.wildcardChild;
1313
+ } else if (segment.startsWith(":")) {
1314
+ const paramName = segment.slice(1);
1315
+ if (!node2.paramChild) {
1316
+ node2.paramChild = this.createNode();
1317
+ node2.paramChild.paramName = paramName;
1318
+ }
1319
+ node2 = node2.paramChild;
1320
+ node2.paramName = paramName;
1321
+ } else {
1322
+ if (!node2.children[segment]) {
1323
+ node2.children[segment] = this.createNode();
1324
+ }
1325
+ node2 = node2.children[segment];
1326
+ }
1327
+ }
1328
+ if (!node2.handlers) {
1329
+ node2.handlers = {};
1330
+ }
1331
+ node2.handlers[method] = handler;
1332
+ }
1333
+ search(method, path2) {
1334
+ const segments = this.splitPath(path2);
1335
+ const params = {};
1336
+ const match = this.findNode(this.root, segments, 0, params);
1337
+ if (match && match.handlers) {
1338
+ const handler = match.handlers[method] || match.handlers["ALL"];
1339
+ if (handler) {
1340
+ return { handler, params };
1341
+ }
1342
+ if (method === "HEAD" && match.handlers["GET"]) {
1343
+ return { handler: match.handlers["GET"], params };
1344
+ }
1345
+ }
1346
+ return null;
1347
+ }
1348
+ findNode(node2, segments, index, params) {
1349
+ if (index === segments.length) {
1350
+ if (node2.handlers) return node2;
1351
+ if (node2.recursiveChild && node2.recursiveChild.handlers) {
1352
+ return node2.recursiveChild;
1353
+ }
1354
+ return null;
1355
+ }
1356
+ const segment = segments[index];
1357
+ const child = node2.children[segment];
1358
+ if (child) {
1359
+ const result = this.findNode(child, segments, index + 1, params);
1360
+ if (result) return result;
1361
+ }
1362
+ if (node2.paramChild) {
1363
+ params[node2.paramChild.paramName] = segment;
1364
+ const result = this.findNode(node2.paramChild, segments, index + 1, params);
1365
+ if (result) return result;
1366
+ delete params[node2.paramChild.paramName];
1367
+ }
1368
+ if (node2.wildcardChild) {
1369
+ const result = this.findNode(node2.wildcardChild, segments, index + 1, params);
1370
+ if (result) return result;
1371
+ }
1372
+ if (node2.recursiveChild) {
1373
+ const remaining = segments.length - index;
1374
+ for (let k = 0; k <= remaining; k++) {
1375
+ const result = this.findNode(node2.recursiveChild, segments, index + k, params);
1376
+ if (result) return result;
1377
+ }
1378
+ }
1379
+ return null;
1380
+ }
1381
+ splitPath(path2) {
1382
+ if (path2 === "/" || path2 === "") return [];
1383
+ const s = path2.startsWith("/") ? path2.slice(1) : path2;
1384
+ if (s === "") return [];
1385
+ return s.split("/");
1386
+ }
1127
1387
  }
1128
1388
  const asyncContext = new node_async_hooks.AsyncLocalStorage();
1389
+ const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1390
+ const db = new surrealdb.Surreal({
1391
+ engines: node.createNodeEngines()
1392
+ });
1393
+ const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
1394
+ return db.query(`
1395
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1396
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1397
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1398
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1399
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1400
+ `);
1401
+ });
1402
+ const datastore = {
1403
+ get(store, key) {
1404
+ return db.select(new surrealdb.RecordId(store, key));
1405
+ },
1406
+ set(store, key, value) {
1407
+ return db.create(new surrealdb.RecordId(store, key)).content(value);
1408
+ },
1409
+ async query(query, vars) {
1410
+ try {
1411
+ const r = await db.query(query, vars).collect();
1412
+ return r;
1413
+ } catch (e) {
1414
+ console.error("DS ERROR:", e);
1415
+ throw e;
1416
+ }
1417
+ },
1418
+ ready
1419
+ };
1420
+ process.on("exit", async () => {
1421
+ await db.close();
1422
+ });
1423
+ const tracer = api.trace.getTracer("shokupan.middleware");
1424
+ function traceHandler(fn, name) {
1425
+ return async function(...args) {
1426
+ return tracer.startActiveSpan(`route handler - ${name}`, {
1427
+ kind: api.SpanKind.INTERNAL,
1428
+ attributes: {
1429
+ "http.route": name,
1430
+ "component": "shokupan.route"
1431
+ }
1432
+ }, async (span) => {
1433
+ try {
1434
+ const result = await fn.apply(this, args);
1435
+ return result;
1436
+ } catch (err) {
1437
+ span.recordException(err);
1438
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
1439
+ throw err;
1440
+ } finally {
1441
+ span.end();
1442
+ }
1443
+ });
1444
+ };
1445
+ }
1446
+ function getCallerInfo(skipFrames = 1) {
1447
+ let file = "unknown";
1448
+ let line = 0;
1449
+ try {
1450
+ const err = new Error();
1451
+ const stack = err.stack?.split("\n") || [];
1452
+ let found = 0;
1453
+ for (let i = 1; i < stack.length; i++) {
1454
+ const l = stack[i];
1455
+ if (!l.includes(":")) continue;
1456
+ if (l.includes("node_modules")) continue;
1457
+ if (l.includes("bun:main")) continue;
1458
+ if (l.includes("src/util/stack.ts")) continue;
1459
+ if (l.includes("src/router.ts")) continue;
1460
+ if (l.includes("src/shokupan.ts")) continue;
1461
+ found++;
1462
+ if (found >= skipFrames) {
1463
+ const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1464
+ if (match) {
1465
+ file = match[1];
1466
+ line = parseInt(match[2], 10);
1467
+ return { file, line };
1468
+ }
1469
+ }
1470
+ }
1471
+ } catch (e) {
1472
+ }
1473
+ return { file, line };
1474
+ }
1129
1475
  const RouterRegistry = /* @__PURE__ */ new Map();
1130
1476
  const ShokupanApplicationTree = {};
1131
1477
  class ShokupanRouter {
@@ -1145,6 +1491,7 @@ class ShokupanRouter {
1145
1491
  [$parent] = null;
1146
1492
  [$childRouters] = [];
1147
1493
  [$childControllers] = [];
1494
+ middleware = [];
1148
1495
  get rootConfig() {
1149
1496
  return this[$appRoot]?.applicationConfig;
1150
1497
  }
@@ -1153,7 +1500,54 @@ class ShokupanRouter {
1153
1500
  }
1154
1501
  [$routes] = [];
1155
1502
  // Public via Symbol for OpenAPI generator
1503
+ trie = new RouterTrie();
1504
+ metadata;
1505
+ // Metadata for the router itself
1156
1506
  currentGuards = [];
1507
+ // Registry Accessor
1508
+ getComponentRegistry() {
1509
+ const routes = this[$routes].map((r) => ({
1510
+ type: "route",
1511
+ path: r.path,
1512
+ method: r.method,
1513
+ metadata: r.metadata,
1514
+ handlerName: r.handler.name,
1515
+ tags: r.handlerSpec?.tags,
1516
+ order: r.order,
1517
+ _fn: r.handler
1518
+ // Expose handler for debugging instrumentation
1519
+ }));
1520
+ const mw = this.middleware;
1521
+ const middleware = mw ? mw.map((m) => ({
1522
+ name: m.name || "middleware",
1523
+ metadata: m.metadata,
1524
+ order: m.order,
1525
+ _fn: m
1526
+ // Expose function for debugging instrumentation
1527
+ })) : [];
1528
+ const routers = this[$childRouters].map((r) => ({
1529
+ type: "router",
1530
+ path: r[$mountPath],
1531
+ metadata: r.metadata,
1532
+ children: r.getComponentRegistry()
1533
+ }));
1534
+ const controllers = this[$childControllers].map((c) => {
1535
+ return {
1536
+ type: "controller",
1537
+ path: c[$mountPath] || "/",
1538
+ name: c.constructor.name,
1539
+ metadata: c.metadata
1540
+ // Check if we can store this
1541
+ };
1542
+ });
1543
+ return {
1544
+ metadata: this.metadata,
1545
+ middleware,
1546
+ routes,
1547
+ routers,
1548
+ controllers
1549
+ };
1550
+ }
1157
1551
  isRouterInstance(target) {
1158
1552
  return typeof target === "object" && target !== null && $isRouter in target;
1159
1553
  }
@@ -1179,6 +1573,14 @@ class ShokupanRouter {
1179
1573
  throw new Error("Router is already mounted");
1180
1574
  }
1181
1575
  controller[$mountPath] = prefix;
1576
+ if (!controller.metadata) {
1577
+ const info = getCallerInfo();
1578
+ controller.metadata = {
1579
+ file: info.file,
1580
+ line: info.line,
1581
+ name: "MountedRouter"
1582
+ };
1583
+ }
1182
1584
  this[$childRouters].push(controller);
1183
1585
  controller[$parent] = this;
1184
1586
  const setRouterContext = (router) => {
@@ -1211,6 +1613,12 @@ class ShokupanRouter {
1211
1613
  }
1212
1614
  }
1213
1615
  instance[$mountPath] = prefix;
1616
+ const info = getCallerInfo();
1617
+ instance.metadata = {
1618
+ file: info.file,
1619
+ line: info.line,
1620
+ name: instance.constructor.name
1621
+ };
1214
1622
  this[$childControllers].push(instance);
1215
1623
  const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1216
1624
  const proto = Object.getPrototypeOf(instance);
@@ -1294,14 +1702,39 @@ class ShokupanRouter {
1294
1702
  for (const arg of sortedArgs) {
1295
1703
  switch (arg.type) {
1296
1704
  case RouteParamType.BODY:
1297
- args[arg.index] = await ctx.req.json().catch(() => ({}));
1705
+ try {
1706
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1707
+ args[arg.index] = await ctx.req.json();
1708
+ } else {
1709
+ const text = await ctx.req.text();
1710
+ if (!text) {
1711
+ args[arg.index] = {};
1712
+ } else {
1713
+ args[arg.index] = JSON.parse(text);
1714
+ }
1715
+ }
1716
+ } catch (e) {
1717
+ const err = new Error("Invalid JSON body");
1718
+ err.status = 400;
1719
+ throw err;
1720
+ }
1298
1721
  break;
1299
1722
  case RouteParamType.PARAM:
1300
1723
  args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1301
1724
  break;
1302
1725
  case RouteParamType.QUERY: {
1303
1726
  const url = new URL(ctx.req.url);
1304
- args[arg.index] = arg.name ? url.searchParams.get(arg.name) : Object.fromEntries(url.searchParams);
1727
+ if (arg.name) {
1728
+ const vals = url.searchParams.getAll(arg.name);
1729
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
1730
+ } else {
1731
+ const query = {};
1732
+ for (const key of url.searchParams.keys()) {
1733
+ const vals = url.searchParams.getAll(key);
1734
+ query[key] = vals.length > 1 ? vals : vals[0];
1735
+ }
1736
+ args[arg.index] = query;
1737
+ }
1305
1738
  break;
1306
1739
  }
1307
1740
  case RouteParamType.HEADER:
@@ -1316,7 +1749,7 @@ class ShokupanRouter {
1316
1749
  }
1317
1750
  }
1318
1751
  }
1319
- const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
1752
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1320
1753
  return tracedOriginalHandler.apply(instance, args);
1321
1754
  };
1322
1755
  let finalHandler = wrappedHandler;
@@ -1449,29 +1882,59 @@ class ShokupanRouter {
1449
1882
  data: result
1450
1883
  };
1451
1884
  }
1452
- applyHooks(match) {
1885
+ applyRouterHooks(match) {
1453
1886
  if (!this.config?.hooks) return match;
1454
1887
  const hooks = this.config.hooks;
1455
- const originalHandler = match.handler;
1456
- match.handler = async (ctx) => {
1457
- if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
1458
- try {
1459
- const result = await originalHandler(ctx);
1460
- if (hooks.onRequestEnd) await hooks.onRequestEnd(ctx);
1461
- return result;
1888
+ return {
1889
+ ...match,
1890
+ handler: this.wrapWithHooks(match.handler, hooks)
1891
+ };
1892
+ }
1893
+ wrapWithHooks(handler, hooks) {
1894
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
1895
+ const hasStart = hookList.some((h) => !!h.onRequestStart);
1896
+ const hasEnd = hookList.some((h) => !!h.onRequestEnd);
1897
+ const hasError = hookList.some((h) => !!h.onError);
1898
+ if (!hasStart && !hasEnd && !hasError) return handler;
1899
+ const originalHandler = handler;
1900
+ const wrapped = async (ctx) => {
1901
+ if (hasStart) {
1902
+ for (let i = 0; i < hookList.length; i++) {
1903
+ const h = hookList[i];
1904
+ if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
1905
+ }
1906
+ }
1907
+ const debug = ctx._debug;
1908
+ let debugId;
1909
+ let previousNode;
1910
+ if (debug) {
1911
+ debugId = originalHandler._debugId || originalHandler.name || "handler";
1912
+ previousNode = debug.getCurrentNode();
1913
+ debug.trackEdge(previousNode, debugId);
1914
+ debug.setNode(debugId);
1915
+ }
1916
+ const start = performance.now();
1917
+ try {
1918
+ const res = await originalHandler(ctx);
1919
+ debug?.trackStep(debugId, "handler", performance.now() - start, "success");
1920
+ for (let i = 0; i < hookList.length; i++) {
1921
+ const h = hookList[i];
1922
+ if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
1923
+ }
1924
+ return res;
1462
1925
  } catch (err) {
1463
- if (hooks.onError) {
1464
- try {
1465
- await hooks.onError(err, ctx);
1466
- } catch (e) {
1467
- console.error("Error in router onError hook:", e);
1468
- }
1926
+ debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
1927
+ for (let i = 0; i < hookList.length; i++) {
1928
+ const h = hookList[i];
1929
+ if (typeof h.onError === "function") await h.onError(err, ctx);
1469
1930
  }
1470
1931
  throw err;
1932
+ } finally {
1933
+ if (debug && previousNode) debug.setNode(previousNode);
1471
1934
  }
1472
1935
  };
1473
- match.handler.originalHandler = originalHandler.originalHandler || originalHandler;
1474
- return match;
1936
+ wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
1937
+ return wrapped;
1475
1938
  }
1476
1939
  /**
1477
1940
  * Find a route matching the given method and path.
@@ -1480,29 +1943,24 @@ class ShokupanRouter {
1480
1943
  * @returns Route handler and parameters if found, otherwise null
1481
1944
  */
1482
1945
  find(method, path2) {
1483
- for (const route of this[$routes]) {
1484
- if (route.method !== "ALL" && route.method !== method) continue;
1485
- const match = route.regex.exec(path2);
1486
- if (match) {
1487
- const params = {};
1488
- route.keys.forEach((key, index) => {
1489
- params[key] = match[index + 1];
1490
- });
1491
- return this.applyHooks({ handler: route.handler, params });
1492
- }
1946
+ let result = this.trie.search(method, path2);
1947
+ if (result) return result;
1948
+ if (method === "HEAD") {
1949
+ result = this.trie.search("GET", path2);
1950
+ if (result) return result;
1493
1951
  }
1494
1952
  for (const child of this[$childRouters]) {
1495
1953
  const prefix = child[$mountPath];
1496
1954
  if (path2 === prefix || path2.startsWith(prefix + "/")) {
1497
1955
  const subPath = path2.slice(prefix.length) || "/";
1498
1956
  const match = child.find(method, subPath);
1499
- if (match) return this.applyHooks(match);
1957
+ if (match) return this.applyRouterHooks(match);
1500
1958
  }
1501
1959
  if (prefix.endsWith("/")) {
1502
1960
  if (path2.startsWith(prefix)) {
1503
1961
  const subPath = path2.slice(prefix.length) || "/";
1504
1962
  const match = child.find(method, subPath);
1505
- if (match) return this.applyHooks(match);
1963
+ if (match) return this.applyRouterHooks(match);
1506
1964
  }
1507
1965
  }
1508
1966
  }
@@ -1513,7 +1971,7 @@ class ShokupanRouter {
1513
1971
  const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
1514
1972
  keys.push(key);
1515
1973
  return "([^/]+)";
1516
- }).replace(/\*/g, ".*");
1974
+ }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
1517
1975
  return {
1518
1976
  regex: new RegExp(`^${pattern}$`),
1519
1977
  keys
@@ -1582,18 +2040,84 @@ class ShokupanRouter {
1582
2040
  return innerHandler(ctx);
1583
2041
  };
1584
2042
  }
2043
+ const { file, line } = getCallerInfo();
2044
+ const trackingHandler = wrappedHandler;
2045
+ wrappedHandler = async (ctx) => {
2046
+ if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2047
+ return trackingHandler(ctx);
2048
+ }
2049
+ const startTime = performance.now();
2050
+ let error = void 0;
2051
+ try {
2052
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2053
+ ctx.handlerStack.push({
2054
+ name: handler.name || "anonymous",
2055
+ file,
2056
+ line
2057
+ });
2058
+ }
2059
+ return await trackingHandler(ctx);
2060
+ } catch (e) {
2061
+ error = e;
2062
+ throw e;
2063
+ } finally {
2064
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2065
+ const duration = performance.now() - startTime;
2066
+ const config = ctx.app.applicationConfig;
2067
+ try {
2068
+ const timestamp = Date.now();
2069
+ const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2070
+ await datastore.set("middleware_tracking", key, {
2071
+ name: handler.name || "anonymous",
2072
+ path: ctx.path,
2073
+ timestamp,
2074
+ duration,
2075
+ file,
2076
+ line,
2077
+ error: error ? String(error) : void 0,
2078
+ metadata: {
2079
+ isBuiltin: handler.isBuiltin,
2080
+ pluginName: handler.pluginName
2081
+ }
2082
+ });
2083
+ const ttl = config.middlewareTrackingTTL ?? 864e5;
2084
+ const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2085
+ const cutoff = Date.now() - ttl;
2086
+ await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2087
+ const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2088
+ if (results && results[0] && results[0].count > maxCapacity) {
2089
+ const toDelete = results[0].count - maxCapacity;
2090
+ await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2091
+ }
2092
+ } catch (datastoreError) {
2093
+ console.error("Failed to store middleware tracking:", datastoreError);
2094
+ }
2095
+ }
2096
+ }
2097
+ };
2098
+ wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2099
+ let bakedHandler = wrappedHandler;
2100
+ if (this.config?.hooks) {
2101
+ bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2102
+ }
1585
2103
  this[$routes].push({
1586
2104
  method,
1587
2105
  path: path2,
1588
- regex,
1589
- keys,
1590
- handler: wrappedHandler,
2106
+ regex: regex ?? new RegExp(""),
2107
+ keys: keys ?? [],
2108
+ handler,
2109
+ bakedHandler,
1591
2110
  handlerSpec: spec,
1592
2111
  group,
1593
- guards: routeGuards.length > 0 ? routeGuards : void 0,
1594
- requestTimeout: effectiveTimeout
1595
- // Save for inspection? Or just relying on closure
2112
+ hooks: this.config?.hooks,
2113
+ requestTimeout,
2114
+ renderer,
2115
+ metadata: {
2116
+ file,
2117
+ line
2118
+ }
1596
2119
  });
2120
+ this.trie.insert(method, path2, bakedHandler);
1597
2121
  return this;
1598
2122
  }
1599
2123
  get(path2, ...args) {
@@ -1627,7 +2151,35 @@ class ShokupanRouter {
1627
2151
  guard(specOrHandler, handler) {
1628
2152
  const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
1629
2153
  const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
1630
- this.currentGuards.push({ handler: guardHandler, spec });
2154
+ let file = "unknown";
2155
+ let line = 0;
2156
+ try {
2157
+ const err = new Error();
2158
+ const stack = err.stack?.split("\n") || [];
2159
+ const callerLine = stack.find(
2160
+ (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
2161
+ );
2162
+ if (callerLine) {
2163
+ const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
2164
+ if (match) {
2165
+ file = match[1];
2166
+ line = parseInt(match[2], 10);
2167
+ }
2168
+ }
2169
+ } catch (e) {
2170
+ }
2171
+ const trackedGuard = async (ctx, next) => {
2172
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2173
+ ctx.handlerStack.push({
2174
+ name: guardHandler.name || "guard",
2175
+ file,
2176
+ line
2177
+ });
2178
+ }
2179
+ return guardHandler(ctx, next);
2180
+ };
2181
+ trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
2182
+ this.currentGuards.push({ handler: trackedGuard, spec });
1631
2183
  return this;
1632
2184
  }
1633
2185
  /**
@@ -1639,10 +2191,10 @@ class ShokupanRouter {
1639
2191
  const config = typeof options === "string" ? { root: options } : options;
1640
2192
  const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
1641
2193
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1642
- serveStatic(null, config, prefix);
2194
+ const handlerMiddleware = serveStatic(config, prefix);
1643
2195
  const routeHandler = async (ctx) => {
1644
- const runner = serveStatic(ctx, config, prefix);
1645
- return runner();
2196
+ return handlerMiddleware(ctx, async () => {
2197
+ });
1646
2198
  };
1647
2199
  let groupName = "Static";
1648
2200
  const segments = normalizedPrefix.split("/").filter(Boolean);
@@ -1703,6 +2255,49 @@ class ShokupanRouter {
1703
2255
  return generateOpenApi(this, options);
1704
2256
  }
1705
2257
  }
2258
+ class SystemCpuMonitor {
2259
+ constructor(intervalMs = 1e3) {
2260
+ this.intervalMs = intervalMs;
2261
+ }
2262
+ interval = null;
2263
+ lastCpus = [];
2264
+ currentUsage = 0;
2265
+ start() {
2266
+ if (this.interval) return;
2267
+ this.lastCpus = os__namespace.cpus();
2268
+ this.interval = setInterval(() => this.update(), this.intervalMs);
2269
+ }
2270
+ stop() {
2271
+ if (this.interval) {
2272
+ clearInterval(this.interval);
2273
+ this.interval = null;
2274
+ }
2275
+ }
2276
+ getUsage() {
2277
+ return this.currentUsage;
2278
+ }
2279
+ update() {
2280
+ const cpus = os__namespace.cpus();
2281
+ let idle = 0;
2282
+ let total = 0;
2283
+ for (let i = 0; i < cpus.length; i++) {
2284
+ const cpu = cpus[i];
2285
+ const prev = this.lastCpus[i];
2286
+ let type;
2287
+ for (type in cpu.times) {
2288
+ const ticks = cpu.times[type];
2289
+ const prevTicks = prev.times[type];
2290
+ const diff = ticks - prevTicks;
2291
+ total += diff;
2292
+ if (type === "idle") {
2293
+ idle += diff;
2294
+ }
2295
+ }
2296
+ }
2297
+ this.lastCpus = cpus;
2298
+ this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
2299
+ }
2300
+ }
1706
2301
  const defaults = {
1707
2302
  port: 3e3,
1708
2303
  hostname: "localhost",
@@ -1714,21 +2309,59 @@ api.trace.getTracer("shokupan.application");
1714
2309
  class Shokupan extends ShokupanRouter {
1715
2310
  applicationConfig = {};
1716
2311
  openApiSpec;
1717
- middleware = [];
2312
+ composedMiddleware;
2313
+ cpuMonitor;
2314
+ hookCache = /* @__PURE__ */ new Map();
2315
+ hooksInitialized = false;
1718
2316
  get logger() {
1719
2317
  return this.applicationConfig.logger;
1720
2318
  }
1721
2319
  constructor(applicationConfig = {}) {
1722
- super();
2320
+ const config = Object.assign({}, defaults, applicationConfig);
2321
+ const { hooks, ...routerConfig } = config;
2322
+ super(routerConfig);
1723
2323
  this[$isApplication] = true;
1724
2324
  this[$appRoot] = this;
1725
- Object.assign(this.applicationConfig, defaults, applicationConfig);
2325
+ this.applicationConfig = config;
2326
+ const { file, line } = getCallerInfo();
2327
+ this.metadata = {
2328
+ file,
2329
+ line,
2330
+ name: "ShokupanApplication"
2331
+ };
1726
2332
  }
1727
2333
  /**
1728
2334
  * Adds middleware to the application.
1729
2335
  */
1730
2336
  use(middleware) {
1731
- this.middleware.push(middleware);
2337
+ let trackedMiddleware = middleware;
2338
+ const { file, line } = getCallerInfo();
2339
+ if (!middleware.metadata) {
2340
+ middleware.metadata = {
2341
+ file,
2342
+ line,
2343
+ name: middleware.name || "middleware",
2344
+ isBuiltin: middleware.isBuiltin,
2345
+ pluginName: middleware.pluginName
2346
+ };
2347
+ }
2348
+ trackedMiddleware = async (ctx, next) => {
2349
+ const c = ctx;
2350
+ if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2351
+ const metadata = middleware.metadata || {};
2352
+ c.handlerStack.push({
2353
+ name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2354
+ file: metadata.file || file,
2355
+ line: metadata.line || line,
2356
+ isBuiltin: metadata.isBuiltin
2357
+ });
2358
+ }
2359
+ return middleware(ctx, next);
2360
+ };
2361
+ trackedMiddleware.metadata = middleware.metadata;
2362
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2363
+ trackedMiddleware.order = this.middleware.length;
2364
+ this.middleware.push(trackedMiddleware);
1732
2365
  return this;
1733
2366
  }
1734
2367
  startupHooks = [];
@@ -1739,6 +2372,15 @@ class Shokupan extends ShokupanRouter {
1739
2372
  this.startupHooks.push(callback);
1740
2373
  return this;
1741
2374
  }
2375
+ specAvailableHooks = [];
2376
+ /**
2377
+ * Registers a callback to be executed when the OpenAPI spec is available.
2378
+ * This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
2379
+ */
2380
+ onSpecAvailable(callback) {
2381
+ this.specAvailableHooks.push(callback);
2382
+ return this;
2383
+ }
1742
2384
  /**
1743
2385
  * Starts the application server.
1744
2386
  *
@@ -1755,17 +2397,43 @@ class Shokupan extends ShokupanRouter {
1755
2397
  }
1756
2398
  if (this.applicationConfig.enableOpenApiGen) {
1757
2399
  this.openApiSpec = await generateOpenApi(this);
2400
+ for (const hook of this.specAvailableHooks) {
2401
+ await hook(this.openApiSpec);
2402
+ }
1758
2403
  }
1759
2404
  if (port === 0 && process.platform === "linux") ;
2405
+ if (this.applicationConfig.autoBackpressureFeedback) {
2406
+ this.cpuMonitor = new SystemCpuMonitor();
2407
+ this.cpuMonitor.start();
2408
+ }
1760
2409
  const serveOptions = {
1761
2410
  port: finalPort,
1762
2411
  hostname: this.applicationConfig.hostname,
1763
2412
  development: this.applicationConfig.development,
1764
2413
  fetch: this.fetch.bind(this),
1765
2414
  reusePort: this.applicationConfig.reusePort,
1766
- idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0
2415
+ idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
2416
+ websocket: {
2417
+ open(ws) {
2418
+ ws.data?.handler?.open?.(ws);
2419
+ },
2420
+ message(ws, message) {
2421
+ ws.data?.handler?.message?.(ws, message);
2422
+ },
2423
+ drain(ws) {
2424
+ ws.data?.handler?.drain?.(ws);
2425
+ },
2426
+ close(ws, code, reason) {
2427
+ ws.data?.handler?.close?.(ws, code, reason);
2428
+ }
2429
+ }
1767
2430
  };
1768
- const server = this.applicationConfig.serverFactory ? await this.applicationConfig.serverFactory(serveOptions) : Bun.serve(serveOptions);
2431
+ let factory = this.applicationConfig.serverFactory;
2432
+ if (!factory && typeof Bun === "undefined") {
2433
+ const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-fVKP60e0.cjs"));
2434
+ factory = createHttpServer();
2435
+ }
2436
+ const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
1769
2437
  console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
1770
2438
  return server;
1771
2439
  }
@@ -1820,110 +2488,165 @@ class Shokupan extends ShokupanRouter {
1820
2488
  * @returns The response to send.
1821
2489
  */
1822
2490
  async fetch(req, server) {
1823
- const tracer2 = api.trace.getTracer("shokupan.application");
1824
- const store = asyncContext.getStore();
1825
- const attrs = {
1826
- attributes: {
1827
- "http.url": req.url,
1828
- "http.method": req.method
1829
- }
1830
- };
1831
- const parent = store?.get("span");
1832
- const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
1833
- return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2491
+ if (this.applicationConfig.enableTracing) {
2492
+ const tracer2 = api.trace.getTracer("shokupan.application");
2493
+ const store = asyncContext.getStore();
2494
+ const attrs = {
2495
+ attributes: {
2496
+ "http.url": req.url,
2497
+ "http.method": req.method
2498
+ }
2499
+ };
2500
+ const parent = store?.get("span");
2501
+ const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
2502
+ return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2503
+ const ctxMap = /* @__PURE__ */ new Map();
2504
+ ctxMap.set("span", span);
2505
+ ctxMap.set("request", req);
2506
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
2507
+ });
2508
+ }
2509
+ if (this.applicationConfig.enableAsyncLocalStorage) {
1834
2510
  const ctxMap = /* @__PURE__ */ new Map();
1835
- ctxMap.set("span", span);
1836
2511
  ctxMap.set("request", req);
1837
- const runCallback = () => {
1838
- const request = req;
1839
- const ctx2 = new ShokupanContext(request, server, void 0, this);
1840
- const handle = async () => {
1841
- try {
1842
- if (this.applicationConfig.hooks?.onRequestStart) {
1843
- await this.applicationConfig.hooks.onRequestStart(ctx2);
1844
- }
1845
- const fn = compose(this.middleware);
1846
- const result = await fn(ctx2, async () => {
1847
- const match = this.find(req.method, ctx2.path);
1848
- if (match) {
1849
- ctx2.params = match.params;
1850
- return match.handler(ctx2);
1851
- }
1852
- return null;
1853
- });
1854
- let response;
1855
- if (result instanceof Response) {
1856
- response = result;
1857
- } else if (result === null || result === void 0) {
1858
- span.setAttribute("http.status_code", 404);
1859
- response = ctx2.text("Not Found", 404);
1860
- } else if (typeof result === "object") {
1861
- response = ctx2.json(result);
1862
- } else {
1863
- response = ctx2.text(String(result));
1864
- }
1865
- if (this.applicationConfig.hooks?.onRequestEnd) {
1866
- await this.applicationConfig.hooks.onRequestEnd(ctx2);
1867
- }
1868
- if (this.applicationConfig.hooks?.onResponseStart) {
1869
- await this.applicationConfig.hooks.onResponseStart(ctx2, response);
1870
- }
1871
- return response;
1872
- } catch (err) {
1873
- console.error(err);
1874
- span.recordException(err);
1875
- span.setStatus({ code: 2 });
1876
- const status = err.status || err.statusCode || 500;
1877
- const body = { error: err.message || "Internal Server Error" };
1878
- if (err.errors) body.errors = err.errors;
1879
- if (this.applicationConfig.hooks?.onError) {
1880
- try {
1881
- await this.applicationConfig.hooks.onError(err, ctx2);
1882
- } catch (hookErr) {
1883
- console.error("Error in onError hook:", hookErr);
1884
- }
1885
- }
1886
- return ctx2.json(body, status);
1887
- }
1888
- };
1889
- let executionPromise = handle();
1890
- const timeoutMs = this.applicationConfig.requestTimeout;
1891
- if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
1892
- let timeoutId;
1893
- const timeoutPromise = new Promise((_, reject) => {
1894
- timeoutId = setTimeout(async () => {
1895
- try {
1896
- if (this.applicationConfig.hooks?.onRequestTimeout) {
1897
- await this.applicationConfig.hooks.onRequestTimeout(ctx2);
1898
- }
1899
- } catch (e) {
1900
- console.error("Error in onRequestTimeout hook:", e);
1901
- }
1902
- reject(new Error("Request Timeout"));
1903
- }, timeoutMs);
1904
- });
1905
- executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2512
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
2513
+ }
2514
+ return this.handleRequest(req, server);
2515
+ }
2516
+ async handleRequest(req, server) {
2517
+ const request = req;
2518
+ const controller = new AbortController();
2519
+ const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
2520
+ const handle = async () => {
2521
+ if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2522
+ const msg = "Too Many Requests (CPU Backpressure)";
2523
+ const res = ctx.text(msg, 429);
2524
+ await this.executeHook("onResponseEnd", ctx, res);
2525
+ return res;
2526
+ }
2527
+ try {
2528
+ if (this.hasHook("onRequestStart")) {
2529
+ await this.executeHook("onRequestStart", ctx);
1906
2530
  }
1907
- return executionPromise.catch((err) => {
1908
- if (err.message === "Request Timeout") {
1909
- return ctx2.text("Request Timeout", 408);
2531
+ const fn = this.composedMiddleware ??= compose(this.middleware);
2532
+ const result = await fn(ctx, async () => {
2533
+ const match = this.find(req.method, ctx.path);
2534
+ if (match) {
2535
+ ctx.params = match.params;
2536
+ return match.handler(ctx);
1910
2537
  }
1911
- console.error("Unexpected error in request execution:", err);
1912
- return ctx2.text("Internal Server Error", 500);
1913
- }).then(async (res) => {
1914
- if (this.applicationConfig.hooks?.onResponseEnd) {
1915
- await this.applicationConfig.hooks.onResponseEnd(ctx2, res);
2538
+ return null;
2539
+ });
2540
+ let response;
2541
+ if (result instanceof Response) {
2542
+ response = result;
2543
+ } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2544
+ response = ctx._finalResponse;
2545
+ } else if (result === null || result === void 0) {
2546
+ if (ctx._finalResponse instanceof Response) {
2547
+ response = ctx._finalResponse;
2548
+ } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
2549
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2550
+ } else {
2551
+ response = ctx.text("Not Found", 404);
1916
2552
  }
1917
- return res;
1918
- }).finally(() => span.end());
1919
- };
1920
- if (this.applicationConfig.enableAsyncLocalStorage) {
1921
- return asyncContext.run(ctxMap, runCallback);
1922
- } else {
1923
- return runCallback();
2553
+ } else if (typeof result === "object") {
2554
+ response = ctx.json(result);
2555
+ } else {
2556
+ response = ctx.text(String(result));
2557
+ }
2558
+ if (this.hasHook("onRequestEnd")) {
2559
+ await this.executeHook("onRequestEnd", ctx);
2560
+ }
2561
+ if (this.hasHook("onResponseStart")) {
2562
+ await this.executeHook("onResponseStart", ctx, response);
2563
+ }
2564
+ return response;
2565
+ } catch (err) {
2566
+ console.error(err);
2567
+ const span = asyncContext.getStore()?.get("span");
2568
+ if (span) span.setStatus({ code: 2 });
2569
+ const status = err.status || err.statusCode || 500;
2570
+ const body = { error: err.message || "Internal Server Error" };
2571
+ if (err.errors) body.errors = err.errors;
2572
+ if (this.hasHook("onError")) {
2573
+ await this.executeHook("onError", err, ctx);
2574
+ }
2575
+ return ctx.json(body, status);
1924
2576
  }
2577
+ };
2578
+ let executionPromise = handle();
2579
+ const timeoutMs = this.applicationConfig.requestTimeout;
2580
+ if (timeoutMs && timeoutMs > 0) {
2581
+ let timeoutId;
2582
+ const timeoutPromise = new Promise((_, reject) => {
2583
+ timeoutId = setTimeout(async () => {
2584
+ controller.abort();
2585
+ if (this.hasHook("onRequestTimeout")) {
2586
+ await this.executeHook("onRequestTimeout", ctx);
2587
+ }
2588
+ reject(new Error("Request Timeout"));
2589
+ }, timeoutMs);
2590
+ });
2591
+ executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2592
+ }
2593
+ return executionPromise.catch((err) => {
2594
+ if (err.message === "Request Timeout") {
2595
+ return ctx.text("Request Timeout", 408);
2596
+ }
2597
+ console.error("Unexpected error in request execution:", err);
2598
+ return ctx.text("Internal Server Error", 500);
2599
+ }).then(async (res) => {
2600
+ if (this.hasHook("onResponseEnd")) {
2601
+ await this.executeHook("onResponseEnd", ctx, res);
2602
+ }
2603
+ return res;
1925
2604
  });
1926
2605
  }
2606
+ ensureHooksInitialized() {
2607
+ const hooks = this.applicationConfig.hooks;
2608
+ if (hooks) {
2609
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2610
+ const hookTypes = [
2611
+ "onRequestStart",
2612
+ "onRequestEnd",
2613
+ "onResponseStart",
2614
+ "onResponseEnd",
2615
+ "onError",
2616
+ "beforeValidate",
2617
+ "afterValidate",
2618
+ "onRequestTimeout",
2619
+ "onReadTimeout",
2620
+ "onWriteTimeout"
2621
+ ];
2622
+ for (const type of hookTypes) {
2623
+ const fns = [];
2624
+ for (const h of hookList) {
2625
+ if (h[type]) fns.push(h[type]);
2626
+ }
2627
+ if (fns.length > 0) {
2628
+ this.hookCache.set(type, fns);
2629
+ }
2630
+ }
2631
+ }
2632
+ this.hooksInitialized = true;
2633
+ }
2634
+ async executeHook(name, ...args) {
2635
+ if (!this.hooksInitialized) {
2636
+ this.ensureHooksInitialized();
2637
+ }
2638
+ const fns = this.hookCache.get(name);
2639
+ if (!fns) return;
2640
+ for (const fn of fns) {
2641
+ await fn(...args);
2642
+ }
2643
+ }
2644
+ hasHook(name) {
2645
+ if (!this.hooksInitialized) {
2646
+ this.ensureHooksInitialized();
2647
+ }
2648
+ return this.hookCache.has(name);
2649
+ }
1927
2650
  }
1928
2651
  class AuthPlugin extends ShokupanRouter {
1929
2652
  constructor(authConfig) {
@@ -2128,7 +2851,7 @@ class AuthPlugin extends ShokupanRouter {
2128
2851
  /**
2129
2852
  * Middleware to verify JWT
2130
2853
  */
2131
- middleware() {
2854
+ getMiddleware() {
2132
2855
  return async (ctx, next) => {
2133
2856
  const authHeader = ctx.req.headers.get("Authorization");
2134
2857
  let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
@@ -2148,19 +2871,44 @@ class AuthPlugin extends ShokupanRouter {
2148
2871
  }
2149
2872
  }
2150
2873
  function Compression(options = {}) {
2151
- const threshold = options.threshold ?? 1024;
2152
- return async (ctx, next) => {
2874
+ const threshold = options.threshold ?? 512;
2875
+ const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
2153
2876
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
2154
2877
  let method = null;
2155
2878
  if (acceptEncoding.includes("br")) method = "br";
2156
- else if (acceptEncoding.includes("gzip")) method = "gzip";
2879
+ else if (acceptEncoding.includes("zstd")) {
2880
+ if (typeof Bun === "undefined") {
2881
+ throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
2882
+ }
2883
+ method = "zstd";
2884
+ } else if (acceptEncoding.includes("gzip")) method = "gzip";
2157
2885
  else if (acceptEncoding.includes("deflate")) method = "deflate";
2158
2886
  if (!method) return next();
2159
- const response = await next();
2887
+ let response = await next();
2888
+ if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
2889
+ response = ctx._finalResponse;
2890
+ }
2160
2891
  if (response instanceof Response) {
2161
2892
  if (response.headers.has("Content-Encoding")) return response;
2162
- const body = await response.arrayBuffer();
2163
- if (body.byteLength < threshold) {
2893
+ let body;
2894
+ let bodySize;
2895
+ if (ctx._rawBody !== void 0) {
2896
+ if (typeof ctx._rawBody === "string") {
2897
+ const encoded = new TextEncoder().encode(ctx._rawBody);
2898
+ body = encoded.buffer;
2899
+ bodySize = encoded.byteLength;
2900
+ } else if (ctx._rawBody instanceof Uint8Array) {
2901
+ body = ctx._rawBody.buffer;
2902
+ bodySize = ctx._rawBody.byteLength;
2903
+ } else {
2904
+ body = ctx._rawBody;
2905
+ bodySize = ctx._rawBody.byteLength;
2906
+ }
2907
+ } else {
2908
+ body = await response.arrayBuffer();
2909
+ bodySize = body.byteLength;
2910
+ }
2911
+ if (bodySize < threshold) {
2164
2912
  return new Response(body, {
2165
2913
  status: response.status,
2166
2914
  statusText: response.statusText,
@@ -2168,17 +2916,36 @@ function Compression(options = {}) {
2168
2916
  });
2169
2917
  }
2170
2918
  let compressed;
2171
- if (method === "br") {
2172
- compressed = require("node:zlib").brotliCompressSync(body);
2173
- } else if (method === "gzip") {
2174
- compressed = Bun.gzipSync(body);
2175
- } else {
2176
- compressed = Bun.deflateSync(body);
2919
+ switch (method) {
2920
+ case "br":
2921
+ compressed = await new Promise((res, rej) => zlib__namespace.brotliCompress(body, {
2922
+ params: {
2923
+ [zlib__namespace.constants.BROTLI_PARAM_QUALITY]: 4
2924
+ }
2925
+ }, (err, data) => {
2926
+ if (err) return rej(err);
2927
+ res(data);
2928
+ }));
2929
+ break;
2930
+ case "gzip":
2931
+ compressed = await new Promise((res, rej) => zlib__namespace.gzip(body, (err, data) => {
2932
+ if (err) return rej(err);
2933
+ res(data);
2934
+ }));
2935
+ break;
2936
+ case "zstd":
2937
+ compressed = await Bun.zstdCompress(body);
2938
+ break;
2939
+ default:
2940
+ compressed = await new Promise((res, rej) => zlib__namespace.deflate(body, (err, data) => {
2941
+ if (err) return rej(err);
2942
+ res(data);
2943
+ }));
2944
+ break;
2177
2945
  }
2178
2946
  const headers = new Headers(response.headers);
2179
2947
  headers.set("Content-Encoding", method);
2180
2948
  headers.set("Content-Length", String(compressed.length));
2181
- headers.delete("Content-Length");
2182
2949
  return new Response(compressed, {
2183
2950
  status: response.status,
2184
2951
  statusText: response.statusText,
@@ -2187,6 +2954,9 @@ function Compression(options = {}) {
2187
2954
  }
2188
2955
  return response;
2189
2956
  };
2957
+ compressionMiddleware.isBuiltin = true;
2958
+ compressionMiddleware.pluginName = "Compression";
2959
+ return compressionMiddleware;
2190
2960
  }
2191
2961
  function Cors(options = {}) {
2192
2962
  const defaults2 = {
@@ -2196,7 +2966,7 @@ function Cors(options = {}) {
2196
2966
  optionsSuccessStatus: 204
2197
2967
  };
2198
2968
  const opts = { ...defaults2, ...options };
2199
- return async (ctx, next) => {
2969
+ const corsMiddleware = async function CorsMiddleware(ctx, next) {
2200
2970
  const headers = new Headers();
2201
2971
  const origin = ctx.headers.get("origin");
2202
2972
  const set = (k, v) => headers.set(k, v);
@@ -2258,6 +3028,9 @@ function Cors(options = {}) {
2258
3028
  }
2259
3029
  return response;
2260
3030
  };
3031
+ corsMiddleware.isBuiltin = true;
3032
+ corsMiddleware.pluginName = "Cors";
3033
+ return corsMiddleware;
2261
3034
  }
2262
3035
  function useExpress(expressMiddleware) {
2263
3036
  return async (ctx, next) => {
@@ -2319,122 +3092,409 @@ function useExpress(expressMiddleware) {
2319
3092
  });
2320
3093
  };
2321
3094
  }
2322
- function RateLimit(options = {}) {
2323
- const windowMs = options.windowMs || 60 * 1e3;
2324
- const max = options.max || 5;
2325
- const message = options.message || "Too many requests, please try again later.";
2326
- const statusCode = options.statusCode || 429;
2327
- const headers = options.headers !== false;
2328
- const keyGenerator = options.keyGenerator || ((ctx) => {
2329
- return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
2330
- });
2331
- const skip = options.skip || (() => false);
2332
- const hits = /* @__PURE__ */ new Map();
2333
- const interval = setInterval(() => {
2334
- const now = Date.now();
2335
- for (const [key, record] of hits.entries()) {
2336
- if (record.resetTime <= now) {
2337
- hits.delete(key);
2338
- }
2339
- }
2340
- }, windowMs);
2341
- if (interval.unref) interval.unref();
2342
- return async (ctx, next) => {
2343
- if (skip(ctx)) return next();
2344
- const key = keyGenerator(ctx);
2345
- const now = Date.now();
2346
- let record = hits.get(key);
2347
- if (!record || record.resetTime <= now) {
2348
- record = {
2349
- hits: 0,
2350
- resetTime: now + windowMs
2351
- };
2352
- hits.set(key, record);
2353
- }
2354
- record.hits++;
2355
- const remaining = Math.max(0, max - record.hits);
2356
- const resetTime = Math.ceil(record.resetTime / 1e3);
2357
- if (record.hits > max) {
2358
- if (headers) {
2359
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2360
- res.headers.set("X-RateLimit-Limit", String(max));
2361
- res.headers.set("X-RateLimit-Remaining", "0");
2362
- res.headers.set("X-RateLimit-Reset", String(resetTime));
2363
- return res;
2364
- }
2365
- return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2366
- }
2367
- const response = await next();
2368
- if (response instanceof Response && headers) {
2369
- response.headers.set("X-RateLimit-Limit", String(max));
2370
- response.headers.set("X-RateLimit-Remaining", String(remaining));
2371
- response.headers.set("X-RateLimit-Reset", String(resetTime));
2372
- }
2373
- return response;
2374
- };
3095
+ class ValidationError extends Error {
3096
+ constructor(errors) {
3097
+ super("Validation Error");
3098
+ this.errors = errors;
3099
+ }
3100
+ status = 400;
2375
3101
  }
2376
- const eta = new eta$2.Eta();
2377
- class ScalarPlugin extends ShokupanRouter {
2378
- constructor(pluginOptions) {
2379
- super();
2380
- this.pluginOptions = pluginOptions;
2381
- this.init();
3102
+ function isZod(schema) {
3103
+ return typeof schema?.safeParse === "function";
3104
+ }
3105
+ async function validateZod(schema, data) {
3106
+ const result = await schema.safeParseAsync(data);
3107
+ if (!result.success) {
3108
+ throw new ValidationError(result.error.errors);
2382
3109
  }
2383
- init() {
2384
- this.get("/", (ctx) => {
2385
- let path2 = ctx.url.toString();
2386
- if (!path2.endsWith("/")) path2 += "/";
2387
- return ctx.html(eta.renderString(`<!doctype html>
2388
- <html>
2389
- <head>
2390
- <title>API Reference</title>
2391
- <meta charset = "utf-8" />
2392
- <meta name="viewport" content = "width=device-width, initial-scale=1" />
2393
- </head>
2394
-
2395
- <body>
2396
- <div id="app"></div>
2397
-
2398
- <script src="<%= it.path %>scalar.js"><\/script>
2399
- <script>
2400
- Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
2401
- url: "<%= it.path %>openapi.json",
2402
- }
2403
- ])
2404
- <\/script>
2405
- </body>
2406
-
2407
- </html>`, { path: path2, config: this.pluginOptions }));
2408
- });
2409
- this.get("/scalar.js", (ctx) => {
2410
- return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
2411
- });
2412
- this.get("/openapi.json", async (ctx) => {
2413
- let spec;
2414
- if (this.root.openApiSpec) {
2415
- try {
2416
- spec = structuredClone(this.root.openApiSpec);
2417
- } catch (e) {
2418
- spec = Object.assign({}, this.root.openApiSpec);
2419
- }
2420
- } else {
2421
- spec = await (this.root || this).generateApiSpec();
2422
- }
2423
- if (this.pluginOptions.baseDocument) {
2424
- deepMerge(spec, this.pluginOptions.baseDocument);
2425
- }
2426
- return ctx.json(spec);
2427
- });
3110
+ return result.data;
3111
+ }
3112
+ function isTypeBox(schema) {
3113
+ return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
3114
+ }
3115
+ function validateTypeBox(schema, data) {
3116
+ if (!schema.Check(data)) {
3117
+ throw new ValidationError([...schema.Errors(data)]);
2428
3118
  }
2429
- // New lifecycle method to be called by router.mount
2430
- onMount(parent) {
2431
- if (parent.onStart) {
2432
- parent.onStart(async () => {
2433
- if (this.pluginOptions.enableStaticAnalysis) {
2434
- try {
2435
- const entrypoint = process.argv[1];
2436
- console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
2437
- const analyzer = new openapiAnalyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
3119
+ return data;
3120
+ }
3121
+ function isAjv(schema) {
3122
+ return typeof schema === "function" && "errors" in schema;
3123
+ }
3124
+ function validateAjv(schema, data) {
3125
+ const valid = schema(data);
3126
+ if (!valid) {
3127
+ throw new ValidationError(schema.errors);
3128
+ }
3129
+ return data;
3130
+ }
3131
+ const valibot = (schema, parser) => {
3132
+ return {
3133
+ _valibot: true,
3134
+ schema,
3135
+ parser
3136
+ };
3137
+ };
3138
+ function isValibotWrapper(schema) {
3139
+ return schema?._valibot === true;
3140
+ }
3141
+ async function validateValibotWrapper(wrapper, data) {
3142
+ const result = await wrapper.parser(wrapper.schema, data);
3143
+ if (!result.success) {
3144
+ throw new ValidationError(result.issues);
3145
+ }
3146
+ return result.output;
3147
+ }
3148
+ function isClass(schema) {
3149
+ try {
3150
+ if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
3151
+ return true;
3152
+ }
3153
+ return typeof schema === "function" && schema.prototype && schema.name;
3154
+ } catch {
3155
+ return false;
3156
+ }
3157
+ }
3158
+ async function validateClassValidator(schema, data) {
3159
+ const object = classTransformer.plainToInstance(schema, data);
3160
+ try {
3161
+ await classValidator.validateOrReject(object);
3162
+ return object;
3163
+ } catch (errors) {
3164
+ const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
3165
+ property: err.property,
3166
+ constraints: err.constraints,
3167
+ children: err.children
3168
+ })) : errors;
3169
+ throw new ValidationError(formattedErrors);
3170
+ }
3171
+ }
3172
+ const safelyGetBody = async (ctx) => {
3173
+ const req = ctx.req;
3174
+ if (req._bodyParsed) {
3175
+ return req._bodyValue;
3176
+ }
3177
+ try {
3178
+ let data;
3179
+ if (typeof req.json === "function") {
3180
+ data = await req.json();
3181
+ } else {
3182
+ data = req.body;
3183
+ if (typeof data === "string") {
3184
+ try {
3185
+ data = JSON.parse(data);
3186
+ } catch {
3187
+ }
3188
+ }
3189
+ }
3190
+ req._bodyParsed = true;
3191
+ req._bodyValue = data;
3192
+ Object.defineProperty(req, "json", {
3193
+ value: async () => req._bodyValue,
3194
+ configurable: true
3195
+ });
3196
+ return data;
3197
+ } catch (e) {
3198
+ return {};
3199
+ }
3200
+ };
3201
+ function getValidator(schema) {
3202
+ if (isZod(schema)) {
3203
+ return (data) => validateZod(schema, data);
3204
+ }
3205
+ if (isTypeBox(schema)) {
3206
+ return (data) => validateTypeBox(schema, data);
3207
+ }
3208
+ if (isAjv(schema)) {
3209
+ return (data) => validateAjv(schema, data);
3210
+ }
3211
+ if (isValibotWrapper(schema)) {
3212
+ return (data) => validateValibotWrapper(schema, data);
3213
+ }
3214
+ if (isClass(schema)) {
3215
+ return (data) => validateClassValidator(schema, data);
3216
+ }
3217
+ if (typeof schema === "function") {
3218
+ return schema;
3219
+ }
3220
+ throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
3221
+ }
3222
+ function validate(config) {
3223
+ const validators = {};
3224
+ if (config.params) validators.params = getValidator(config.params);
3225
+ if (config.query) validators.query = getValidator(config.query);
3226
+ if (config.headers) validators.headers = getValidator(config.headers);
3227
+ if (config.body) validators.body = getValidator(config.body);
3228
+ return async (ctx, next) => {
3229
+ const dataToValidate = {};
3230
+ if (config.params) dataToValidate.params = ctx.params;
3231
+ let queryObj;
3232
+ if (config.query) {
3233
+ const url = new URL(ctx.req.url);
3234
+ queryObj = Object.fromEntries(url.searchParams.entries());
3235
+ dataToValidate.query = queryObj;
3236
+ }
3237
+ if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
3238
+ let body;
3239
+ if (config.body) {
3240
+ body = await safelyGetBody(ctx);
3241
+ dataToValidate.body = body;
3242
+ }
3243
+ if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
3244
+ await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
3245
+ }
3246
+ if (validators.params) {
3247
+ ctx.params = await validators.params(ctx.params);
3248
+ }
3249
+ let validQuery;
3250
+ if (validators.query && queryObj) {
3251
+ validQuery = await validators.query(queryObj);
3252
+ }
3253
+ if (validators.headers) {
3254
+ const headersObj = Object.fromEntries(ctx.req.headers.entries());
3255
+ await validators.headers(headersObj);
3256
+ }
3257
+ let validBody;
3258
+ if (validators.body) {
3259
+ const b = body ?? await safelyGetBody(ctx);
3260
+ validBody = await validators.body(b);
3261
+ const req = ctx.req;
3262
+ req._bodyValue = validBody;
3263
+ Object.defineProperty(req, "json", {
3264
+ value: async () => validBody,
3265
+ configurable: true
3266
+ });
3267
+ ctx.body = validBody;
3268
+ }
3269
+ if (ctx.app?.applicationConfig.hooks?.afterValidate) {
3270
+ const validatedData = { ...dataToValidate };
3271
+ if (config.params) validatedData.params = ctx.params;
3272
+ if (config.query) validatedData.query = validQuery;
3273
+ if (config.body) validatedData.body = validBody;
3274
+ await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
3275
+ }
3276
+ return next();
3277
+ };
3278
+ }
3279
+ const ajv = new Ajv({ coerceTypes: true, allErrors: true });
3280
+ addFormats(ajv);
3281
+ const compiledValidators = /* @__PURE__ */ new WeakMap();
3282
+ function openApiValidator() {
3283
+ return async (ctx, next) => {
3284
+ const app = ctx.app;
3285
+ if (!app || !app.openApiSpec) {
3286
+ return next();
3287
+ }
3288
+ let cache = compiledValidators.get(app);
3289
+ if (!cache) {
3290
+ cache = compileValidators(app.openApiSpec);
3291
+ compiledValidators.set(app, cache);
3292
+ }
3293
+ let matchPath;
3294
+ let matchParams = {};
3295
+ if (cache.validators.has(ctx.path)) {
3296
+ matchPath = ctx.path;
3297
+ } else {
3298
+ for (const [path2, { regex, paramNames }] of cache.paths) {
3299
+ const match = regex.exec(ctx.path);
3300
+ if (match) {
3301
+ matchPath = path2;
3302
+ paramNames.forEach((name, i) => {
3303
+ matchParams[name] = match[i + 1];
3304
+ });
3305
+ break;
3306
+ }
3307
+ }
3308
+ }
3309
+ if (!matchPath) {
3310
+ return next();
3311
+ }
3312
+ const method = ctx.req.method.toLowerCase();
3313
+ const validators = cache.validators.get(matchPath)?.[method];
3314
+ if (!validators) {
3315
+ return next();
3316
+ }
3317
+ const errors = [];
3318
+ if (validators.body) {
3319
+ let body;
3320
+ try {
3321
+ body = await ctx.req.json().catch(() => ({}));
3322
+ } catch {
3323
+ body = {};
3324
+ }
3325
+ const valid = validators.body(body);
3326
+ if (!valid && validators.body.errors) {
3327
+ errors.push(...validators.body.errors.map((e) => ({ ...e, location: "body" })));
3328
+ }
3329
+ }
3330
+ if (validators.query) {
3331
+ const query = Object.fromEntries(new URL(ctx.req.url).searchParams.entries());
3332
+ const valid = validators.query(query);
3333
+ if (!valid && validators.query.errors) {
3334
+ errors.push(...validators.query.errors.map((e) => ({ ...e, location: "query" })));
3335
+ }
3336
+ }
3337
+ if (validators.params) {
3338
+ const params = { ...matchParams, ...ctx.params };
3339
+ const valid = validators.params(params);
3340
+ if (!valid && validators.params.errors) {
3341
+ errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
3342
+ }
3343
+ }
3344
+ if (validators.headers) {
3345
+ const headers = Object.fromEntries(ctx.req.headers.entries());
3346
+ const valid = validators.headers(headers);
3347
+ if (!valid && validators.headers.errors) {
3348
+ errors.push(...validators.headers.errors.map((e) => ({ ...e, location: "header" })));
3349
+ }
3350
+ }
3351
+ if (errors.length > 0) {
3352
+ throw new ValidationError(errors);
3353
+ }
3354
+ return next();
3355
+ };
3356
+ }
3357
+ function compileValidators(spec) {
3358
+ const validators = /* @__PURE__ */ new Map();
3359
+ const paths = /* @__PURE__ */ new Map();
3360
+ for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
3361
+ if (path2.includes("{")) {
3362
+ const paramNames = [];
3363
+ const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
3364
+ paramNames.push(name);
3365
+ return "([^/]+)";
3366
+ }) + "$";
3367
+ paths.set(path2, {
3368
+ regex: new RegExp(regexStr),
3369
+ paramNames
3370
+ });
3371
+ }
3372
+ const pathValidators = {};
3373
+ for (const [method, operation] of Object.entries(pathItem)) {
3374
+ if (method === "parameters" || method === "summary" || method === "description") continue;
3375
+ const oper = operation;
3376
+ const opValidators = {};
3377
+ if (oper.requestBody?.content?.["application/json"]?.schema) {
3378
+ opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
3379
+ }
3380
+ const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
3381
+ const queryProps = {};
3382
+ const pathProps = {};
3383
+ const headerProps = {};
3384
+ const queryRequired = [];
3385
+ const pathRequired = [];
3386
+ const headerRequired = [];
3387
+ for (const param of parameters) {
3388
+ if (param.in === "query") {
3389
+ queryProps[param.name] = param.schema || {};
3390
+ if (param.required) queryRequired.push(param.name);
3391
+ } else if (param.in === "path") {
3392
+ pathProps[param.name] = param.schema || {};
3393
+ pathRequired.push(param.name);
3394
+ } else if (param.in === "header") {
3395
+ headerProps[param.name] = param.schema || {};
3396
+ if (param.required) headerRequired.push(param.name);
3397
+ }
3398
+ }
3399
+ if (Object.keys(queryProps).length > 0) {
3400
+ opValidators.query = ajv.compile({
3401
+ type: "object",
3402
+ properties: queryProps,
3403
+ required: queryRequired.length > 0 ? queryRequired : void 0
3404
+ });
3405
+ }
3406
+ if (Object.keys(pathProps).length > 0) {
3407
+ opValidators.params = ajv.compile({
3408
+ type: "object",
3409
+ properties: pathProps,
3410
+ required: pathRequired.length > 0 ? pathRequired : void 0
3411
+ });
3412
+ }
3413
+ if (Object.keys(headerProps).length > 0) {
3414
+ opValidators.headers = ajv.compile({
3415
+ type: "object",
3416
+ properties: headerProps,
3417
+ required: headerRequired.length > 0 ? headerRequired : void 0
3418
+ });
3419
+ }
3420
+ pathValidators[method] = opValidators;
3421
+ }
3422
+ validators.set(path2, pathValidators);
3423
+ }
3424
+ return { paths, validators };
3425
+ }
3426
+ function precompileValidators(app, spec) {
3427
+ const cache = compileValidators(spec);
3428
+ compiledValidators.set(app, cache);
3429
+ }
3430
+ function enableOpenApiValidation(app) {
3431
+ app.use(openApiValidator());
3432
+ app.onSpecAvailable((spec) => {
3433
+ precompileValidators(app, spec);
3434
+ });
3435
+ }
3436
+ const eta = new eta$2.Eta();
3437
+ class ScalarPlugin extends ShokupanRouter {
3438
+ constructor(pluginOptions) {
3439
+ super();
3440
+ this.pluginOptions = pluginOptions;
3441
+ this.init();
3442
+ }
3443
+ init() {
3444
+ this.get("/", (ctx) => {
3445
+ let path2 = ctx.url.toString();
3446
+ if (!path2.endsWith("/")) path2 += "/";
3447
+ return ctx.html(eta.renderString(`<!doctype html>
3448
+ <html>
3449
+ <head>
3450
+ <title>API Reference</title>
3451
+ <meta charset = "utf-8" />
3452
+ <meta name="viewport" content = "width=device-width, initial-scale=1" />
3453
+ </head>
3454
+
3455
+ <body>
3456
+ <div id="app"></div>
3457
+
3458
+ <script src="<%= it.path %>scalar.js"><\/script>
3459
+ <script>
3460
+ Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3461
+ url: "<%= it.path %>openapi.json",
3462
+ }
3463
+ ])
3464
+ <\/script>
3465
+ </body>
3466
+
3467
+ </html>`, { path: path2, config: this.pluginOptions }));
3468
+ });
3469
+ this.get("/scalar.js", (ctx) => {
3470
+ return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
3471
+ });
3472
+ this.get("/openapi.json", async (ctx) => {
3473
+ let spec;
3474
+ if (this.root.openApiSpec) {
3475
+ try {
3476
+ spec = structuredClone(this.root.openApiSpec);
3477
+ } catch (e) {
3478
+ spec = Object.assign({}, this.root.openApiSpec);
3479
+ }
3480
+ } else {
3481
+ spec = await (this.root || this).generateApiSpec();
3482
+ }
3483
+ if (this.pluginOptions.baseDocument) {
3484
+ deepMerge(spec, this.pluginOptions.baseDocument);
3485
+ }
3486
+ return ctx.json(spec);
3487
+ });
3488
+ }
3489
+ // New lifecycle method to be called by router.mount
3490
+ onMount(parent) {
3491
+ if (parent.onStart) {
3492
+ parent.onStart(async () => {
3493
+ if (this.pluginOptions.enableStaticAnalysis) {
3494
+ try {
3495
+ const entrypoint = process.argv[1];
3496
+ console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3497
+ const analyzer = new openapiAnalyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
2438
3498
  let staticSpec = await analyzer.analyze();
2439
3499
  if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
2440
3500
  deepMerge(this.pluginOptions.baseDocument, staticSpec);
@@ -2448,7 +3508,7 @@ class ScalarPlugin extends ShokupanRouter {
2448
3508
  }
2449
3509
  }
2450
3510
  function SecurityHeaders(options = {}) {
2451
- return async (ctx, next) => {
3511
+ const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
2452
3512
  const headers = {};
2453
3513
  const set = (k, v) => headers[k] = v;
2454
3514
  if (options.dnsPrefetchControl !== false) {
@@ -2502,6 +3562,9 @@ function SecurityHeaders(options = {}) {
2502
3562
  }
2503
3563
  return response;
2504
3564
  };
3565
+ securityHeadersMiddleware.isBuiltin = true;
3566
+ securityHeadersMiddleware.pluginName = "SecurityHeaders";
3567
+ return securityHeadersMiddleware;
2505
3568
  }
2506
3569
  class Cookie {
2507
3570
  maxAge;
@@ -2615,7 +3678,7 @@ function Session(options) {
2615
3678
  const resave = options.resave === void 0 ? true : options.resave;
2616
3679
  const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
2617
3680
  const rolling = options.rolling || false;
2618
- return async (ctx, next) => {
3681
+ const sessionMiddleware = async function SessionMiddleware(ctx, next) {
2619
3682
  let reqSessionId = null;
2620
3683
  const cookieHeader = ctx.req.headers.get("cookie");
2621
3684
  const cookies = {};
@@ -2751,194 +3814,9 @@ function Session(options) {
2751
3814
  }
2752
3815
  return result;
2753
3816
  };
2754
- }
2755
- class ValidationError extends Error {
2756
- constructor(errors) {
2757
- super("Validation Error");
2758
- this.errors = errors;
2759
- }
2760
- status = 400;
2761
- }
2762
- function isZod(schema) {
2763
- return typeof schema?.safeParse === "function";
2764
- }
2765
- async function validateZod(schema, data) {
2766
- const result = await schema.safeParseAsync(data);
2767
- if (!result.success) {
2768
- throw new ValidationError(result.error.errors);
2769
- }
2770
- return result.data;
2771
- }
2772
- function isTypeBox(schema) {
2773
- return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
2774
- }
2775
- function validateTypeBox(schema, data) {
2776
- if (!schema.Check(data)) {
2777
- throw new ValidationError([...schema.Errors(data)]);
2778
- }
2779
- return data;
2780
- }
2781
- function isAjv(schema) {
2782
- return typeof schema === "function" && "errors" in schema;
2783
- }
2784
- function validateAjv(schema, data) {
2785
- const valid = schema(data);
2786
- if (!valid) {
2787
- throw new ValidationError(schema.errors);
2788
- }
2789
- return data;
2790
- }
2791
- const valibot = (schema, parser) => {
2792
- return {
2793
- _valibot: true,
2794
- schema,
2795
- parser
2796
- };
2797
- };
2798
- function isValibotWrapper(schema) {
2799
- return schema?._valibot === true;
2800
- }
2801
- async function validateValibotWrapper(wrapper, data) {
2802
- const result = await wrapper.parser(wrapper.schema, data);
2803
- if (!result.success) {
2804
- throw new ValidationError(result.issues);
2805
- }
2806
- return result.output;
2807
- }
2808
- function isClass(schema) {
2809
- try {
2810
- if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
2811
- return true;
2812
- }
2813
- return typeof schema === "function" && schema.prototype && schema.name;
2814
- } catch {
2815
- return false;
2816
- }
2817
- }
2818
- async function validateClassValidator(schema, data) {
2819
- const object = classTransformer.plainToInstance(schema, data);
2820
- try {
2821
- await classValidator.validateOrReject(object);
2822
- return object;
2823
- } catch (errors) {
2824
- const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
2825
- property: err.property,
2826
- constraints: err.constraints,
2827
- children: err.children
2828
- })) : errors;
2829
- throw new ValidationError(formattedErrors);
2830
- }
2831
- }
2832
- const safelyGetBody = async (ctx) => {
2833
- const req = ctx.req;
2834
- if (req._bodyParsed) {
2835
- return req._bodyValue;
2836
- }
2837
- try {
2838
- let data;
2839
- if (typeof req.json === "function") {
2840
- data = await req.json();
2841
- } else {
2842
- data = req.body;
2843
- if (typeof data === "string") {
2844
- try {
2845
- data = JSON.parse(data);
2846
- } catch {
2847
- }
2848
- }
2849
- }
2850
- req._bodyParsed = true;
2851
- req._bodyValue = data;
2852
- Object.defineProperty(req, "json", {
2853
- value: async () => req._bodyValue,
2854
- configurable: true
2855
- });
2856
- return data;
2857
- } catch (e) {
2858
- return {};
2859
- }
2860
- };
2861
- function validate(config) {
2862
- return async (ctx, next) => {
2863
- const dataToValidate = {};
2864
- if (config.params) dataToValidate.params = ctx.params;
2865
- let queryObj;
2866
- if (config.query) {
2867
- const url = new URL(ctx.req.url);
2868
- queryObj = Object.fromEntries(url.searchParams.entries());
2869
- dataToValidate.query = queryObj;
2870
- }
2871
- if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
2872
- let body;
2873
- if (config.body) {
2874
- body = await safelyGetBody(ctx);
2875
- dataToValidate.body = body;
2876
- }
2877
- if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2878
- await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2879
- }
2880
- if (config.params) {
2881
- ctx.params = await runValidation(config.params, ctx.params);
2882
- }
2883
- let validQuery;
2884
- if (config.query && queryObj) {
2885
- validQuery = await runValidation(config.query, queryObj);
2886
- }
2887
- if (config.headers) {
2888
- const headersObj = Object.fromEntries(ctx.req.headers.entries());
2889
- await runValidation(config.headers, headersObj);
2890
- }
2891
- let validBody;
2892
- if (config.body) {
2893
- const b = body ?? await safelyGetBody(ctx);
2894
- validBody = await runValidation(config.body, b);
2895
- const req = ctx.req;
2896
- req._bodyValue = validBody;
2897
- Object.defineProperty(req, "json", {
2898
- value: async () => validBody,
2899
- configurable: true
2900
- });
2901
- ctx.body = validBody;
2902
- }
2903
- if (ctx.app?.applicationConfig.hooks?.afterValidate) {
2904
- const validatedData = { ...dataToValidate };
2905
- if (config.params) validatedData.params = ctx.params;
2906
- if (config.query) validatedData.query = validQuery;
2907
- if (config.body) validatedData.body = validBody;
2908
- await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
2909
- }
2910
- return next();
2911
- };
2912
- }
2913
- async function runValidation(schema, data) {
2914
- if (isZod(schema)) {
2915
- return validateZod(schema, data);
2916
- }
2917
- if (isTypeBox(schema)) {
2918
- return validateTypeBox(schema, data);
2919
- }
2920
- if (isAjv(schema)) {
2921
- return validateAjv(schema, data);
2922
- }
2923
- if (isValibotWrapper(schema)) {
2924
- return validateValibotWrapper(schema, data);
2925
- }
2926
- if (isClass(schema)) {
2927
- return validateClassValidator(schema, data);
2928
- }
2929
- if (isTypeBox(schema)) {
2930
- return validateTypeBox(schema, data);
2931
- }
2932
- if (isAjv(schema)) {
2933
- return validateAjv(schema, data);
2934
- }
2935
- if (isValibotWrapper(schema)) {
2936
- return validateValibotWrapper(schema, data);
2937
- }
2938
- if (typeof schema === "function") {
2939
- return schema(data);
2940
- }
2941
- throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
3817
+ sessionMiddleware.isBuiltin = true;
3818
+ sessionMiddleware.pluginName = "Session";
3819
+ return sessionMiddleware;
2942
3820
  }
2943
3821
  exports.$appRoot = $appRoot;
2944
3822
  exports.$childControllers = $childControllers;
@@ -2978,6 +3856,7 @@ exports.Post = Post;
2978
3856
  exports.Put = Put;
2979
3857
  exports.Query = Query;
2980
3858
  exports.RateLimit = RateLimit;
3859
+ exports.RateLimitMiddleware = RateLimitMiddleware;
2981
3860
  exports.Req = Req;
2982
3861
  exports.RouteParamType = RouteParamType;
2983
3862
  exports.RouterRegistry = RouterRegistry;
@@ -2993,7 +3872,11 @@ exports.ShokupanRouter = ShokupanRouter;
2993
3872
  exports.Spec = Spec;
2994
3873
  exports.Use = Use;
2995
3874
  exports.ValidationError = ValidationError;
3875
+ exports.compileValidators = compileValidators;
2996
3876
  exports.compose = compose;
3877
+ exports.enableOpenApiValidation = enableOpenApiValidation;
3878
+ exports.openApiValidator = openApiValidator;
3879
+ exports.precompileValidators = precompileValidators;
2997
3880
  exports.useExpress = useExpress;
2998
3881
  exports.valibot = valibot;
2999
3882
  exports.validate = validate;