shokupan 0.2.0 → 0.4.4

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 (59) hide show
  1. package/README.md +197 -24
  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 +17 -7
  29. package/dist/decorators.d.ts +47 -0
  30. package/dist/index.cjs +939 -385
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +931 -379
  33. package/dist/index.js.map +1 -1
  34. package/dist/{openapi-analyzer-BTExMLX4.js → openapi-analyzer-BtIaHIfe.js} +11 -6
  35. package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
  36. package/dist/{openapi-analyzer-BN0wFCML.cjs → openapi-analyzer-D9YB3IkV.cjs} +11 -6
  37. package/dist/openapi-analyzer-D9YB3IkV.cjs.map +1 -0
  38. package/dist/plugins/auth.d.ts +1 -1
  39. package/dist/plugins/debugview/plugin.d.ts +7 -12
  40. package/dist/plugins/failed-request-recorder.d.ts +14 -0
  41. package/dist/plugins/idempotency/plugin.d.ts +14 -0
  42. package/dist/plugins/openapi-validator.d.ts +28 -0
  43. package/dist/plugins/rate-limit.d.ts +3 -1
  44. package/dist/plugins/serve-static.d.ts +2 -3
  45. package/dist/router/trie.d.ts +14 -0
  46. package/dist/router.d.ts +60 -4
  47. package/dist/{server-adapter-CnQFr4P7.js → server-adapter-BWrEJbKL.js} +5 -5
  48. package/dist/server-adapter-BWrEJbKL.js.map +1 -0
  49. package/dist/{server-adapter-BD6oKEto.cjs → server-adapter-fVKP60e0.cjs} +5 -5
  50. package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
  51. package/dist/shokupan.d.ts +14 -3
  52. package/dist/types.d.ts +100 -4
  53. package/dist/util/cpu-monitor.d.ts +11 -0
  54. package/dist/util/stack.d.ts +8 -0
  55. package/package.json +12 -5
  56. package/dist/openapi-analyzer-BN0wFCML.cjs.map +0 -1
  57. package/dist/openapi-analyzer-BTExMLX4.js.map +0 -1
  58. package/dist/server-adapter-BD6oKEto.cjs.map +0 -1
  59. package/dist/server-adapter-CnQFr4P7.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,15 +1,20 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import { Eta } from "eta";
2
- import { stat, readdir } from "fs/promises";
3
+ import { stat, readdir, readFile as readFile$1 } from "fs/promises";
3
4
  import { resolve, join, basename } from "path";
4
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { createNodeEngines } from "@surrealdb/node";
7
+ import { Surreal, RecordId } from "surrealdb";
5
8
  import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
9
+ import * as os from "node:os";
6
10
  import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
7
11
  import * as jose from "jose";
12
+ import * as zlib from "node:zlib";
8
13
  import Ajv from "ajv";
9
14
  import addFormats from "ajv-formats";
10
15
  import { plainToInstance } from "class-transformer";
11
16
  import { validateOrReject } from "class-validator";
12
- import { OpenAPIAnalyzer } from "./openapi-analyzer-BTExMLX4.js";
17
+ import { OpenAPIAnalyzer } from "./openapi-analyzer-BtIaHIfe.js";
13
18
  import { randomUUID, createHmac } from "crypto";
14
19
  import { EventEmitter } from "events";
15
20
  class ShokupanResponse {
@@ -76,10 +81,12 @@ class ShokupanResponse {
76
81
  }
77
82
  }
78
83
  class ShokupanContext {
79
- constructor(request, server, state, app, enableMiddlewareTracking = false) {
84
+ // Raw body for compression optimization
85
+ constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
80
86
  this.request = request;
81
87
  this.server = server;
82
88
  this.app = app;
89
+ this.signal = signal;
83
90
  this.state = state || {};
84
91
  if (enableMiddlewareTracking) {
85
92
  const self = this;
@@ -103,7 +110,9 @@ class ShokupanContext {
103
110
  state;
104
111
  handlerStack = [];
105
112
  response;
113
+ _debug;
106
114
  _finalResponse;
115
+ _rawBody;
107
116
  get url() {
108
117
  if (!this._url) {
109
118
  const urlString = this.request.url || "http://localhost/";
@@ -152,7 +161,17 @@ class ShokupanContext {
152
161
  * Request query params
153
162
  */
154
163
  get query() {
155
- return Object.fromEntries(this.url.searchParams);
164
+ const q = {};
165
+ for (const [key, value] of this.url.searchParams) {
166
+ if (q[key] === void 0) {
167
+ q[key] = value;
168
+ } else if (Array.isArray(q[key])) {
169
+ q[key].push(value);
170
+ } else {
171
+ q[key] = [q[key], value];
172
+ }
173
+ }
174
+ return q;
156
175
  }
157
176
  /**
158
177
  * Client IP address
@@ -227,17 +246,36 @@ class ShokupanContext {
227
246
  setCookie(name, value, options = {}) {
228
247
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
229
248
  if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
249
+ if (options.domain) cookie += `; Domain=${options.domain}`;
250
+ if (options.path) cookie += `; Path=${options.path || "/"}`;
230
251
  if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
231
252
  if (options.httpOnly) cookie += `; HttpOnly`;
232
253
  if (options.secure) cookie += `; Secure`;
233
- if (options.domain) cookie += `; Domain=${options.domain}`;
234
- if (options.path) cookie += `; Path=${options.path || "/"}`;
235
- if (options.sameSite) {
236
- typeof options.sameSite === "string" ? options.sameSite.toLowerCase() : options.sameSite ? "strict" : "lax";
237
- cookie += `; SameSite=${typeof options.sameSite === "boolean" ? "Strict" : options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1)}`;
254
+ let sameSite = options.sameSite;
255
+ if (sameSite === true) sameSite = "Strict";
256
+ if (sameSite === void 0 || sameSite === false) ;
257
+ else {
258
+ const stringSameSite = typeof sameSite === "string" ? sameSite.toLowerCase() : sameSite;
259
+ switch (stringSameSite) {
260
+ case "lax":
261
+ cookie += "; SameSite=Lax";
262
+ break;
263
+ case "strict":
264
+ cookie += "; SameSite=Strict";
265
+ break;
266
+ case "none":
267
+ cookie += "; SameSite=None";
268
+ break;
269
+ default:
270
+ cookie += "; SameSite=Lax";
271
+ break;
272
+ }
238
273
  }
239
274
  if (options.priority) {
240
- cookie += `; Priority=${options.priority.charAt(0).toUpperCase() + options.priority.slice(1)}`;
275
+ const p = options.priority.toLowerCase();
276
+ if (p === "low") cookie += "; Priority=Low";
277
+ else if (p === "medium") cookie += "; Priority=Medium";
278
+ else if (p === "high") cookie += "; Priority=High";
241
279
  }
242
280
  this.response.append("Set-Cookie", cookie);
243
281
  return this;
@@ -274,6 +312,9 @@ class ShokupanContext {
274
312
  send(body, options) {
275
313
  const headers = this.mergeHeaders(options?.headers);
276
314
  const status = options?.status ?? this.response.status;
315
+ if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
316
+ this._rawBody = body;
317
+ }
277
318
  this._finalResponse = new Response(body, { status, headers });
278
319
  return this._finalResponse;
279
320
  }
@@ -281,11 +322,11 @@ class ShokupanContext {
281
322
  * Read request body
282
323
  */
283
324
  async body() {
284
- const contentType = this.request.headers.get("content-type");
285
- if (contentType?.includes("application/json")) {
325
+ const contentType = this.request.headers.get("content-type") || "";
326
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
286
327
  return this.request.json();
287
328
  }
288
- if (contentType?.includes("multipart/form-data") || contentType?.includes("application/x-www-form-urlencoded")) {
329
+ if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
289
330
  return this.request.formData();
290
331
  }
291
332
  return this.request.text();
@@ -296,6 +337,7 @@ class ShokupanContext {
296
337
  json(data, status, headers) {
297
338
  const finalStatus = status ?? this.response.status;
298
339
  const jsonString = JSON.stringify(data);
340
+ this._rawBody = jsonString;
299
341
  if (!headers && !this.response.hasPopulatedHeaders) {
300
342
  this._finalResponse = new Response(jsonString, {
301
343
  status: finalStatus,
@@ -313,15 +355,16 @@ class ShokupanContext {
313
355
  */
314
356
  text(data, status, headers) {
315
357
  const finalStatus = status ?? this.response.status;
358
+ this._rawBody = data;
316
359
  if (!headers && !this.response.hasPopulatedHeaders) {
317
360
  this._finalResponse = new Response(data, {
318
361
  status: finalStatus,
319
- headers: { "content-type": "text/plain" }
362
+ headers: { "content-type": "text/plain; charset=utf-8" }
320
363
  });
321
364
  return this._finalResponse;
322
365
  }
323
366
  const finalHeaders = this.mergeHeaders(headers);
324
- finalHeaders.set("content-type", "text/plain");
367
+ finalHeaders.set("content-type", "text/plain; charset=utf-8");
325
368
  this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
326
369
  return this._finalResponse;
327
370
  }
@@ -329,9 +372,10 @@ class ShokupanContext {
329
372
  * Respond with HTML content
330
373
  */
331
374
  html(html, status, headers) {
332
- const finalHeaders = this.mergeHeaders(headers);
333
- finalHeaders.set("content-type", "text/html");
334
375
  const finalStatus = status ?? this.response.status;
376
+ const finalHeaders = this.mergeHeaders(headers);
377
+ finalHeaders.set("content-type", "text/html; charset=utf-8");
378
+ this._rawBody = html;
335
379
  this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
336
380
  return this._finalResponse;
337
381
  }
@@ -356,11 +400,20 @@ class ShokupanContext {
356
400
  /**
357
401
  * Respond with a file
358
402
  */
359
- file(path, fileOptions, responseOptions) {
403
+ async file(path, fileOptions, responseOptions) {
360
404
  const headers = this.mergeHeaders(responseOptions?.headers);
361
405
  const status = responseOptions?.status ?? this.response.status;
362
- this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
363
- return this._finalResponse;
406
+ if (typeof Bun !== "undefined") {
407
+ this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
408
+ return this._finalResponse;
409
+ } else {
410
+ const fileBuffer = await readFile(path);
411
+ if (fileOptions?.type) {
412
+ headers.set("content-type", fileOptions.type);
413
+ }
414
+ this._finalResponse = new Response(fileBuffer, { status, headers });
415
+ return this._finalResponse;
416
+ }
364
417
  }
365
418
  /**
366
419
  * JSX Rendering Function
@@ -380,6 +433,74 @@ class ShokupanContext {
380
433
  return this.html(html, status, headers);
381
434
  }
382
435
  }
436
+ function RateLimitMiddleware(options = {}) {
437
+ const windowMs = options.windowMs || 60 * 1e3;
438
+ const max = options.limit || options.max || 5;
439
+ const message = options.message || "Too many requests, please try again later.";
440
+ const statusCode = options.statusCode || 429;
441
+ const headers = options.headers !== false;
442
+ const mode = options.mode || "user";
443
+ const keyGenerator = options.keyGenerator || ((ctx) => {
444
+ if (mode === "absolute") {
445
+ return "global";
446
+ }
447
+ return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
448
+ });
449
+ const skip = options.skip || (() => false);
450
+ const hits = /* @__PURE__ */ new Map();
451
+ const interval = setInterval(() => {
452
+ const now = Date.now();
453
+ for (const [key, record] of hits.entries()) {
454
+ if (record.resetTime <= now) {
455
+ hits.delete(key);
456
+ }
457
+ }
458
+ }, windowMs);
459
+ if (interval.unref) interval.unref();
460
+ const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
461
+ if (skip(ctx)) return next();
462
+ const key = keyGenerator(ctx);
463
+ const now = Date.now();
464
+ let record = hits.get(key);
465
+ if (!record || record.resetTime <= now) {
466
+ record = {
467
+ hits: 0,
468
+ resetTime: now + windowMs
469
+ };
470
+ hits.set(key, record);
471
+ }
472
+ record.hits++;
473
+ const remaining = Math.max(0, max - record.hits);
474
+ const resetTime = Math.ceil(record.resetTime / 1e3);
475
+ const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
476
+ const setHeaders = (res) => {
477
+ if (!headers || !res || !res.headers) return;
478
+ try {
479
+ res.headers.set("X-RateLimit-Limit", String(max));
480
+ res.headers.set("X-RateLimit-Remaining", String(remaining));
481
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
482
+ } catch (e) {
483
+ }
484
+ };
485
+ if (record.hits > max) {
486
+ typeof message === "object" ? JSON.stringify(message) : String(message);
487
+ const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
488
+ if (headers) {
489
+ setHeaders(res);
490
+ res.headers.set("Retry-After", String(retryAfter));
491
+ }
492
+ return res;
493
+ }
494
+ const response = await next();
495
+ if (response instanceof Response && headers) {
496
+ setHeaders(response);
497
+ }
498
+ return response;
499
+ };
500
+ rateLimitMiddleware.isBuiltin = true;
501
+ rateLimitMiddleware.pluginName = "RateLimit";
502
+ return rateLimitMiddleware;
503
+ }
383
504
  const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
384
505
  const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
385
506
  const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
@@ -476,6 +597,9 @@ const Patch = createMethodDecorator("PATCH");
476
597
  const Options = createMethodDecorator("OPTIONS");
477
598
  const Head = createMethodDecorator("HEAD");
478
599
  const All = createMethodDecorator("ALL");
600
+ function RateLimit(options) {
601
+ return Use(RateLimitMiddleware(options));
602
+ }
479
603
  class Container {
480
604
  static services = /* @__PURE__ */ new Map();
481
605
  static register(target, instance) {
@@ -517,17 +641,31 @@ const compose = (middleware) => {
517
641
  }
518
642
  return function dispatch(context2, next) {
519
643
  let index = -1;
520
- function runner(i) {
644
+ async function runner(i) {
521
645
  if (i <= index) return Promise.reject(new Error("next() called multiple times"));
522
646
  index = i;
523
647
  if (i >= middleware.length) {
524
648
  return next ? next() : Promise.resolve();
525
649
  }
526
650
  const fn = middleware[i];
651
+ if (!context2._debug) {
652
+ return fn(context2, () => runner(i + 1));
653
+ }
654
+ const debug = context2._debug;
655
+ const debugId = fn._debugId || fn.name || "anonymous";
656
+ const previousNode = debug.getCurrentNode();
657
+ debug.trackEdge(previousNode, debugId);
658
+ debug.setNode(debugId);
659
+ const start = performance.now();
527
660
  try {
528
- return Promise.resolve(fn(context2, () => runner(i + 1)));
661
+ const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
662
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
663
+ return res;
529
664
  } catch (err) {
665
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
530
666
  return Promise.reject(err);
667
+ } finally {
668
+ if (previousNode) debug.setNode(previousNode);
531
669
  }
532
670
  }
533
671
  return runner(0);
@@ -589,6 +727,15 @@ function deepMerge(target, ...sources) {
589
727
  }
590
728
  return deepMerge(target, ...sources);
591
729
  }
730
+ const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
731
+ const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
732
+ const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
733
+ const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
734
+ const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
735
+ const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
736
+ const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
737
+ const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
738
+ const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
592
739
  function analyzeHandler(handler) {
593
740
  const handlerSource = handler.toString();
594
741
  const inferredSpec = {};
@@ -598,46 +745,28 @@ function analyzeHandler(handler) {
598
745
  };
599
746
  }
600
747
  const queryParams = /* @__PURE__ */ new Map();
601
- const queryIntMatch = handlerSource.match(/parseInt\(ctx\.query\.(\w+)\)/g);
602
- if (queryIntMatch) {
603
- queryIntMatch.forEach((match) => {
604
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
605
- if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
606
- });
748
+ for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
749
+ if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
607
750
  }
608
- const queryFloatMatch = handlerSource.match(/parseFloat\(ctx\.query\.(\w+)\)/g);
609
- if (queryFloatMatch) {
610
- queryFloatMatch.forEach((match) => {
611
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
612
- if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
613
- });
751
+ for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
752
+ if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
614
753
  }
615
- const queryNumberMatch = handlerSource.match(/Number\(ctx\.query\.(\w+)\)/g);
616
- if (queryNumberMatch) {
617
- queryNumberMatch.forEach((match) => {
618
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
619
- if (paramName && !queryParams.has(paramName)) {
620
- queryParams.set(paramName, { type: "number" });
621
- }
622
- });
754
+ for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
755
+ if (match[1] && !queryParams.has(match[1])) {
756
+ queryParams.set(match[1], { type: "number" });
757
+ }
623
758
  }
624
- const queryBoolMatch = handlerSource.match(/(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g);
625
- if (queryBoolMatch) {
626
- queryBoolMatch.forEach((match) => {
627
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
628
- if (paramName && !queryParams.has(paramName)) {
629
- queryParams.set(paramName, { type: "boolean" });
630
- }
631
- });
759
+ for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
760
+ const name = match[1] || match[2];
761
+ if (name && !queryParams.has(name)) {
762
+ queryParams.set(name, { type: "boolean" });
763
+ }
632
764
  }
633
- const queryMatch = handlerSource.match(/ctx\.query\.(\w+)/g);
634
- if (queryMatch) {
635
- queryMatch.forEach((match) => {
636
- const paramName = match.split(".")[2];
637
- if (paramName && !queryParams.has(paramName)) {
638
- queryParams.set(paramName, { type: "string" });
639
- }
640
- });
765
+ for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
766
+ const name = match[1];
767
+ if (name && !queryParams.has(name)) {
768
+ queryParams.set(name, { type: "string" });
769
+ }
641
770
  }
642
771
  if (queryParams.size > 0) {
643
772
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -650,19 +779,11 @@ function analyzeHandler(handler) {
650
779
  });
651
780
  }
652
781
  const pathParams = /* @__PURE__ */ new Map();
653
- const paramIntMatch = handlerSource.match(/parseInt\(ctx\.params\.(\w+)\)/g);
654
- if (paramIntMatch) {
655
- paramIntMatch.forEach((match) => {
656
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
657
- if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
658
- });
782
+ for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
783
+ if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
659
784
  }
660
- const paramFloatMatch = handlerSource.match(/parseFloat\(ctx\.params\.(\w+)\)/g);
661
- if (paramFloatMatch) {
662
- paramFloatMatch.forEach((match) => {
663
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
664
- if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
665
- });
785
+ for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
786
+ if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
666
787
  }
667
788
  if (pathParams.size > 0) {
668
789
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -675,76 +796,55 @@ function analyzeHandler(handler) {
675
796
  });
676
797
  });
677
798
  }
678
- const headerMatch = handlerSource.match(/ctx\.get\(['"](\w+)['"]\)/g);
679
- if (headerMatch) {
680
- if (!inferredSpec.parameters) inferredSpec.parameters = [];
681
- headerMatch.forEach((match) => {
682
- const headerName = match.match(/['"](\w+)['"]/)?.[1];
683
- if (headerName) {
684
- inferredSpec.parameters.push({
685
- name: headerName,
686
- in: "header",
687
- schema: { type: "string" }
688
- });
689
- }
690
- });
799
+ for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
800
+ if (match[1]) {
801
+ if (!inferredSpec.parameters) inferredSpec.parameters = [];
802
+ inferredSpec.parameters.push({
803
+ name: match[1],
804
+ in: "header",
805
+ schema: { type: "string" }
806
+ });
807
+ }
691
808
  }
692
809
  const responses = {};
693
810
  if (handlerSource.includes("ctx.json(")) {
694
811
  responses["200"] = {
695
812
  description: "Successful response",
696
- content: {
697
- "application/json": { schema: { type: "object" } }
698
- }
813
+ content: { "application/json": { schema: { type: "object" } } }
699
814
  };
700
815
  }
701
816
  if (handlerSource.includes("ctx.html(")) {
702
817
  responses["200"] = {
703
818
  description: "Successful response",
704
- content: {
705
- "text/html": { schema: { type: "string" } }
706
- }
819
+ content: { "text/html": { schema: { type: "string" } } }
707
820
  };
708
821
  }
709
822
  if (handlerSource.includes("ctx.text(")) {
710
823
  responses["200"] = {
711
824
  description: "Successful response",
712
- content: {
713
- "text/plain": { schema: { type: "string" } }
714
- }
825
+ content: { "text/plain": { schema: { type: "string" } } }
715
826
  };
716
827
  }
717
828
  if (handlerSource.includes("ctx.file(")) {
718
829
  responses["200"] = {
719
830
  description: "File download",
720
- content: {
721
- "application/octet-stream": { schema: { type: "string", format: "binary" } }
722
- }
831
+ content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
723
832
  };
724
833
  }
725
834
  if (handlerSource.includes("ctx.redirect(")) {
726
- responses["302"] = {
727
- description: "Redirect"
728
- };
835
+ responses["302"] = { description: "Redirect" };
729
836
  }
730
837
  if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
731
838
  responses["200"] = {
732
839
  description: "Successful response",
733
- content: {
734
- "application/json": { schema: { type: "object" } }
735
- }
840
+ content: { "application/json": { schema: { type: "object" } } }
736
841
  };
737
842
  }
738
- const errorStatusMatch = handlerSource.match(/ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g);
739
- if (errorStatusMatch) {
740
- errorStatusMatch.forEach((match) => {
741
- const statusCode = match.match(/,\s*(\d{3,})\)/)?.[1];
742
- if (statusCode && statusCode !== "200") {
743
- responses[statusCode] = {
744
- description: `Error response (${statusCode})`
745
- };
746
- }
747
- });
843
+ for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
844
+ const statusCode = match[1];
845
+ if (statusCode && statusCode !== "200") {
846
+ responses[statusCode] = { description: `Error response (${statusCode})` };
847
+ }
748
848
  }
749
849
  if (Object.keys(responses).length > 0) {
750
850
  inferredSpec.responses = responses;
@@ -758,7 +858,7 @@ async function generateOpenApi(rootRouter, options = {}) {
758
858
  const defaultTagName = options.defaultTag || "Application";
759
859
  let astRoutes = [];
760
860
  try {
761
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-BTExMLX4.js");
861
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-BtIaHIfe.js");
762
862
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
763
863
  const { applications } = await analyzer.analyze();
764
864
  const appMap = /* @__PURE__ */ new Map();
@@ -1028,10 +1128,10 @@ async function generateOpenApi(rootRouter, options = {}) {
1028
1128
  };
1029
1129
  }
1030
1130
  const eta$1 = new Eta();
1031
- function serveStatic(ctx, config, prefix) {
1131
+ function serveStatic(config, prefix) {
1032
1132
  const rootPath = resolve(config.root || ".");
1033
1133
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1034
- return async () => {
1134
+ const serveStaticMiddleware = async (ctx) => {
1035
1135
  let relative = ctx.path.slice(normalizedPrefix.length);
1036
1136
  if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
1037
1137
  if (relative.length === 0) relative = "/";
@@ -1148,16 +1248,158 @@ function serveStatic(ctx, config, prefix) {
1148
1248
  }
1149
1249
  }
1150
1250
  }
1151
- const file = Bun.file(finalPath);
1152
- let response = new Response(file);
1251
+ let response;
1252
+ if (typeof Bun !== "undefined") {
1253
+ response = new Response(Bun.file(finalPath));
1254
+ } else {
1255
+ const fileBuffer = await readFile$1(finalPath);
1256
+ response = new Response(fileBuffer);
1257
+ }
1153
1258
  if (config.hooks?.onResponse) {
1154
1259
  const hooked = await config.hooks.onResponse(ctx, response);
1155
1260
  if (hooked) response = hooked;
1156
1261
  }
1157
1262
  return response;
1158
1263
  };
1264
+ serveStaticMiddleware.isBuiltin = true;
1265
+ serveStaticMiddleware.pluginName = "ServeStatic";
1266
+ return serveStaticMiddleware;
1267
+ }
1268
+ class RouterTrie {
1269
+ root;
1270
+ constructor() {
1271
+ this.root = this.createNode();
1272
+ }
1273
+ createNode() {
1274
+ return {
1275
+ children: {}
1276
+ };
1277
+ }
1278
+ insert(method, path, handler) {
1279
+ let node = this.root;
1280
+ const segments = this.splitPath(path);
1281
+ for (const segment of segments) {
1282
+ if (segment === "**") {
1283
+ if (!node.recursiveChild) {
1284
+ node.recursiveChild = this.createNode();
1285
+ }
1286
+ node = node.recursiveChild;
1287
+ } else if (segment === "*") {
1288
+ if (!node.wildcardChild) {
1289
+ node.wildcardChild = this.createNode();
1290
+ }
1291
+ node = node.wildcardChild;
1292
+ } else if (segment.startsWith(":")) {
1293
+ const paramName = segment.slice(1);
1294
+ if (!node.paramChild) {
1295
+ node.paramChild = this.createNode();
1296
+ node.paramChild.paramName = paramName;
1297
+ }
1298
+ node = node.paramChild;
1299
+ node.paramName = paramName;
1300
+ } else {
1301
+ if (!node.children[segment]) {
1302
+ node.children[segment] = this.createNode();
1303
+ }
1304
+ node = node.children[segment];
1305
+ }
1306
+ }
1307
+ if (!node.handlers) {
1308
+ node.handlers = {};
1309
+ }
1310
+ node.handlers[method] = handler;
1311
+ }
1312
+ search(method, path) {
1313
+ const segments = this.splitPath(path);
1314
+ const params = {};
1315
+ const match = this.findNode(this.root, segments, 0, params);
1316
+ if (match && match.handlers) {
1317
+ const handler = match.handlers[method] || match.handlers["ALL"];
1318
+ if (handler) {
1319
+ return { handler, params };
1320
+ }
1321
+ if (method === "HEAD" && match.handlers["GET"]) {
1322
+ return { handler: match.handlers["GET"], params };
1323
+ }
1324
+ }
1325
+ return null;
1326
+ }
1327
+ findNode(node, segments, index, params) {
1328
+ if (index === segments.length) {
1329
+ if (node.handlers) return node;
1330
+ if (node.recursiveChild && node.recursiveChild.handlers) {
1331
+ return node.recursiveChild;
1332
+ }
1333
+ return null;
1334
+ }
1335
+ const segment = segments[index];
1336
+ const child = node.children[segment];
1337
+ if (child) {
1338
+ const result = this.findNode(child, segments, index + 1, params);
1339
+ if (result) return result;
1340
+ }
1341
+ if (node.paramChild) {
1342
+ params[node.paramChild.paramName] = segment;
1343
+ const result = this.findNode(node.paramChild, segments, index + 1, params);
1344
+ if (result) return result;
1345
+ delete params[node.paramChild.paramName];
1346
+ }
1347
+ if (node.wildcardChild) {
1348
+ const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1349
+ if (result) return result;
1350
+ }
1351
+ if (node.recursiveChild) {
1352
+ const remaining = segments.length - index;
1353
+ for (let k = 0; k <= remaining; k++) {
1354
+ const result = this.findNode(node.recursiveChild, segments, index + k, params);
1355
+ if (result) return result;
1356
+ }
1357
+ }
1358
+ return null;
1359
+ }
1360
+ splitPath(path) {
1361
+ if (path === "/" || path === "") return [];
1362
+ const s = path.startsWith("/") ? path.slice(1) : path;
1363
+ if (s === "") return [];
1364
+ return s.split("/");
1365
+ }
1159
1366
  }
1160
1367
  const asyncContext = new AsyncLocalStorage();
1368
+ const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1369
+ const db = new Surreal({
1370
+ engines: createNodeEngines()
1371
+ });
1372
+ const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
1373
+ return db.query(`
1374
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1375
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1376
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1377
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1378
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1379
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1380
+ `);
1381
+ });
1382
+ const datastore = {
1383
+ get(store, key) {
1384
+ return db.select(new RecordId(store, key));
1385
+ },
1386
+ set(store, key, value) {
1387
+ return db.create(new RecordId(store, key)).content(value);
1388
+ },
1389
+ async query(query, vars) {
1390
+ try {
1391
+ const r = await db.query(query, vars).collect();
1392
+ return r;
1393
+ } catch (e) {
1394
+ console.error("DS ERROR:", e);
1395
+ throw e;
1396
+ }
1397
+ },
1398
+ ready
1399
+ };
1400
+ process.on("exit", async () => {
1401
+ await db.close();
1402
+ });
1161
1403
  const tracer = trace.getTracer("shokupan.middleware");
1162
1404
  function traceHandler(fn, name) {
1163
1405
  return async function(...args) {
@@ -1181,6 +1423,35 @@ function traceHandler(fn, name) {
1181
1423
  });
1182
1424
  };
1183
1425
  }
1426
+ function getCallerInfo(skipFrames = 1) {
1427
+ let file = "unknown";
1428
+ let line = 0;
1429
+ try {
1430
+ const err = new Error();
1431
+ const stack = err.stack?.split("\n") || [];
1432
+ let found = 0;
1433
+ for (let i = 1; i < stack.length; i++) {
1434
+ const l = stack[i];
1435
+ if (!l.includes(":")) continue;
1436
+ if (l.includes("node_modules")) continue;
1437
+ if (l.includes("bun:main")) continue;
1438
+ if (l.includes("src/util/stack.ts")) continue;
1439
+ if (l.includes("src/router.ts")) continue;
1440
+ if (l.includes("src/shokupan.ts")) continue;
1441
+ found++;
1442
+ if (found >= skipFrames) {
1443
+ const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1444
+ if (match) {
1445
+ file = match[1];
1446
+ line = parseInt(match[2], 10);
1447
+ return { file, line };
1448
+ }
1449
+ }
1450
+ }
1451
+ } catch (e) {
1452
+ }
1453
+ return { file, line };
1454
+ }
1184
1455
  const RouterRegistry = /* @__PURE__ */ new Map();
1185
1456
  const ShokupanApplicationTree = {};
1186
1457
  class ShokupanRouter {
@@ -1200,6 +1471,7 @@ class ShokupanRouter {
1200
1471
  [$parent] = null;
1201
1472
  [$childRouters] = [];
1202
1473
  [$childControllers] = [];
1474
+ middleware = [];
1203
1475
  get rootConfig() {
1204
1476
  return this[$appRoot]?.applicationConfig;
1205
1477
  }
@@ -1208,7 +1480,66 @@ class ShokupanRouter {
1208
1480
  }
1209
1481
  [$routes] = [];
1210
1482
  // Public via Symbol for OpenAPI generator
1483
+ trie = new RouterTrie();
1484
+ metadata;
1485
+ // Metadata for the router itself
1211
1486
  currentGuards = [];
1487
+ // Registry Accessor
1488
+ getComponentRegistry() {
1489
+ const controllerRoutesMap = /* @__PURE__ */ new Map();
1490
+ const localRoutes = [];
1491
+ for (const r of this[$routes]) {
1492
+ const entry = {
1493
+ type: "route",
1494
+ path: r.path,
1495
+ method: r.method,
1496
+ metadata: r.metadata,
1497
+ handlerName: r.handler.name,
1498
+ tags: r.handlerSpec?.tags,
1499
+ order: r.order,
1500
+ _fn: r.handler
1501
+ };
1502
+ if (r.controller) {
1503
+ if (!controllerRoutesMap.has(r.controller)) {
1504
+ controllerRoutesMap.set(r.controller, []);
1505
+ }
1506
+ controllerRoutesMap.get(r.controller).push(entry);
1507
+ } else {
1508
+ localRoutes.push(entry);
1509
+ }
1510
+ }
1511
+ const mw = this.middleware;
1512
+ const middleware = mw ? mw.map((m) => ({
1513
+ name: m.name || "middleware",
1514
+ metadata: m.metadata,
1515
+ order: m.order,
1516
+ _fn: m
1517
+ // Expose function for debugging instrumentation
1518
+ })) : [];
1519
+ const routers = this[$childRouters].map((r) => ({
1520
+ type: "router",
1521
+ path: r[$mountPath],
1522
+ metadata: r.metadata,
1523
+ children: r.getComponentRegistry()
1524
+ }));
1525
+ const controllers = this[$childControllers].map((c) => {
1526
+ const routes = controllerRoutesMap.get(c) || [];
1527
+ return {
1528
+ type: "controller",
1529
+ path: c[$mountPath] || "/",
1530
+ name: c.constructor.name,
1531
+ metadata: c.metadata,
1532
+ children: { routes }
1533
+ };
1534
+ });
1535
+ return {
1536
+ metadata: this.metadata,
1537
+ middleware,
1538
+ routes: localRoutes,
1539
+ routers,
1540
+ controllers
1541
+ };
1542
+ }
1212
1543
  isRouterInstance(target) {
1213
1544
  return typeof target === "object" && target !== null && $isRouter in target;
1214
1545
  }
@@ -1234,6 +1565,14 @@ class ShokupanRouter {
1234
1565
  throw new Error("Router is already mounted");
1235
1566
  }
1236
1567
  controller[$mountPath] = prefix;
1568
+ if (!controller.metadata) {
1569
+ const info = getCallerInfo();
1570
+ controller.metadata = {
1571
+ file: info.file,
1572
+ line: info.line,
1573
+ name: "MountedRouter"
1574
+ };
1575
+ }
1237
1576
  this[$childRouters].push(controller);
1238
1577
  controller[$parent] = this;
1239
1578
  const setRouterContext = (router) => {
@@ -1266,6 +1605,12 @@ class ShokupanRouter {
1266
1605
  }
1267
1606
  }
1268
1607
  instance[$mountPath] = prefix;
1608
+ const info = getCallerInfo();
1609
+ instance.metadata = {
1610
+ file: info.file,
1611
+ line: info.line,
1612
+ name: instance.constructor.name
1613
+ };
1269
1614
  this[$childControllers].push(instance);
1270
1615
  const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1271
1616
  const proto = Object.getPrototypeOf(instance);
@@ -1280,7 +1625,7 @@ class ShokupanRouter {
1280
1625
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1281
1626
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1282
1627
  let routesAttached = 0;
1283
- for (const name of methods) {
1628
+ for (const name of Array.from(methods)) {
1284
1629
  if (name === "constructor") continue;
1285
1630
  if (["arguments", "caller", "callee"].includes(name)) continue;
1286
1631
  const originalHandler = instance[name];
@@ -1349,14 +1694,39 @@ class ShokupanRouter {
1349
1694
  for (const arg of sortedArgs) {
1350
1695
  switch (arg.type) {
1351
1696
  case RouteParamType.BODY:
1352
- args[arg.index] = await ctx.req.json().catch(() => ({}));
1697
+ try {
1698
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1699
+ args[arg.index] = await ctx.req.json();
1700
+ } else {
1701
+ const text = await ctx.req.text();
1702
+ if (!text) {
1703
+ args[arg.index] = {};
1704
+ } else {
1705
+ args[arg.index] = JSON.parse(text);
1706
+ }
1707
+ }
1708
+ } catch (e) {
1709
+ const err = new Error("Invalid JSON body");
1710
+ err.status = 400;
1711
+ throw err;
1712
+ }
1353
1713
  break;
1354
1714
  case RouteParamType.PARAM:
1355
1715
  args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1356
1716
  break;
1357
1717
  case RouteParamType.QUERY: {
1358
1718
  const url = new URL(ctx.req.url);
1359
- args[arg.index] = arg.name ? url.searchParams.get(arg.name) : Object.fromEntries(url.searchParams);
1719
+ if (arg.name) {
1720
+ const vals = url.searchParams.getAll(arg.name);
1721
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
1722
+ } else {
1723
+ const query = {};
1724
+ for (const key of url.searchParams.keys()) {
1725
+ const vals = url.searchParams.getAll(key);
1726
+ query[key] = vals.length > 1 ? vals : vals[0];
1727
+ }
1728
+ args[arg.index] = query;
1729
+ }
1360
1730
  break;
1361
1731
  }
1362
1732
  case RouteParamType.HEADER:
@@ -1389,7 +1759,7 @@ class ShokupanRouter {
1389
1759
  const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
1390
1760
  const userSpec = decoratedSpecs && decoratedSpecs.get(name);
1391
1761
  const spec = { tags: [tagName], ...userSpec };
1392
- this.add({ method, path: normalizedPath, handler: finalHandler, spec });
1762
+ this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
1393
1763
  }
1394
1764
  }
1395
1765
  if (routesAttached === 0) {
@@ -1504,30 +1874,59 @@ class ShokupanRouter {
1504
1874
  data: result
1505
1875
  };
1506
1876
  }
1507
- applyHooks(match) {
1877
+ applyRouterHooks(match) {
1508
1878
  if (!this.config?.hooks) return match;
1509
1879
  const hooks = this.config.hooks;
1510
- if (!hooks.onRequestStart && !hooks.onRequestEnd && !hooks.onError) return match;
1511
- const originalHandler = match.handler;
1512
- match.handler = async (ctx) => {
1513
- if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
1880
+ return {
1881
+ ...match,
1882
+ handler: this.wrapWithHooks(match.handler, hooks)
1883
+ };
1884
+ }
1885
+ wrapWithHooks(handler, hooks) {
1886
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
1887
+ const hasStart = hookList.some((h) => !!h.onRequestStart);
1888
+ const hasEnd = hookList.some((h) => !!h.onRequestEnd);
1889
+ const hasError = hookList.some((h) => !!h.onError);
1890
+ if (!hasStart && !hasEnd && !hasError) return handler;
1891
+ const originalHandler = handler;
1892
+ const wrapped = async (ctx) => {
1893
+ if (hasStart) {
1894
+ for (let i = 0; i < hookList.length; i++) {
1895
+ const h = hookList[i];
1896
+ if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
1897
+ }
1898
+ }
1899
+ const debug = ctx._debug;
1900
+ let debugId;
1901
+ let previousNode;
1902
+ if (debug) {
1903
+ debugId = originalHandler._debugId || originalHandler.name || "handler";
1904
+ previousNode = debug.getCurrentNode();
1905
+ debug.trackEdge(previousNode, debugId);
1906
+ debug.setNode(debugId);
1907
+ }
1908
+ const start = performance.now();
1514
1909
  try {
1515
- const result = await originalHandler(ctx);
1516
- if (hooks.onRequestEnd) await hooks.onRequestEnd(ctx);
1517
- return result;
1910
+ const res = await originalHandler(ctx);
1911
+ debug?.trackStep(debugId, "handler", performance.now() - start, "success");
1912
+ for (let i = 0; i < hookList.length; i++) {
1913
+ const h = hookList[i];
1914
+ if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
1915
+ }
1916
+ return res;
1518
1917
  } catch (err) {
1519
- if (hooks.onError) {
1520
- try {
1521
- await hooks.onError(err, ctx);
1522
- } catch (e) {
1523
- console.error("Error in router onError hook:", e);
1524
- }
1918
+ debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
1919
+ for (let i = 0; i < hookList.length; i++) {
1920
+ const h = hookList[i];
1921
+ if (typeof h.onError === "function") await h.onError(err, ctx);
1525
1922
  }
1526
1923
  throw err;
1924
+ } finally {
1925
+ if (debug && previousNode) debug.setNode(previousNode);
1527
1926
  }
1528
1927
  };
1529
- match.handler.originalHandler = originalHandler.originalHandler || originalHandler;
1530
- return match;
1928
+ wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
1929
+ return wrapped;
1531
1930
  }
1532
1931
  /**
1533
1932
  * Find a route matching the given method and path.
@@ -1536,24 +1935,10 @@ class ShokupanRouter {
1536
1935
  * @returns Route handler and parameters if found, otherwise null
1537
1936
  */
1538
1937
  find(method, path) {
1539
- const findInRoutes = (routes, m) => {
1540
- for (const route of routes) {
1541
- if (route.method !== "ALL" && route.method !== m) continue;
1542
- const match = route.regex.exec(path);
1543
- if (match) {
1544
- const params = {};
1545
- route.keys.forEach((key, index) => {
1546
- params[key] = match[index + 1];
1547
- });
1548
- return this.applyHooks({ handler: route.handler, params });
1549
- }
1550
- }
1551
- return null;
1552
- };
1553
- let result = findInRoutes(this[$routes], method);
1938
+ let result = this.trie.search(method, path);
1554
1939
  if (result) return result;
1555
1940
  if (method === "HEAD") {
1556
- result = findInRoutes(this[$routes], "GET");
1941
+ result = this.trie.search("GET", path);
1557
1942
  if (result) return result;
1558
1943
  }
1559
1944
  for (const child of this[$childRouters]) {
@@ -1561,13 +1946,13 @@ class ShokupanRouter {
1561
1946
  if (path === prefix || path.startsWith(prefix + "/")) {
1562
1947
  const subPath = path.slice(prefix.length) || "/";
1563
1948
  const match = child.find(method, subPath);
1564
- if (match) return this.applyHooks(match);
1949
+ if (match) return this.applyRouterHooks(match);
1565
1950
  }
1566
1951
  if (prefix.endsWith("/")) {
1567
1952
  if (path.startsWith(prefix)) {
1568
1953
  const subPath = path.slice(prefix.length) || "/";
1569
1954
  const match = child.find(method, subPath);
1570
- if (match) return this.applyHooks(match);
1955
+ if (match) return this.applyRouterHooks(match);
1571
1956
  }
1572
1957
  }
1573
1958
  }
@@ -1578,7 +1963,7 @@ class ShokupanRouter {
1578
1963
  const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
1579
1964
  keys.push(key);
1580
1965
  return "([^/]+)";
1581
- }).replace(/\*/g, ".*");
1966
+ }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
1582
1967
  return {
1583
1968
  regex: new RegExp(`^${pattern}$`),
1584
1969
  keys
@@ -1595,8 +1980,23 @@ class ShokupanRouter {
1595
1980
  * @param handler - Route handler function
1596
1981
  * @param requestTimeout - Timeout for this route in milliseconds
1597
1982
  */
1598
- add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer }) {
1983
+ add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
1599
1984
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
1985
+ if (this.currentGuards.length > 0) {
1986
+ spec = spec || {};
1987
+ for (const guard of this.currentGuards) {
1988
+ if (guard.spec) {
1989
+ if (guard.spec.responses) {
1990
+ spec.responses = spec.responses || {};
1991
+ Object.assign(spec.responses, guard.spec.responses);
1992
+ }
1993
+ if (guard.spec.security) {
1994
+ spec.security = spec.security || [];
1995
+ spec.security.push(...guard.spec.security);
1996
+ }
1997
+ }
1998
+ }
1999
+ }
1600
2000
  let wrappedHandler = handler;
1601
2001
  const routeGuards = [...this.currentGuards];
1602
2002
  const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
@@ -1647,47 +2047,85 @@ class ShokupanRouter {
1647
2047
  return innerHandler(ctx);
1648
2048
  };
1649
2049
  }
1650
- let file = "unknown";
1651
- let line = 0;
1652
- try {
1653
- const err = new Error();
1654
- const stack = err.stack?.split("\n") || [];
1655
- const callerLine = stack.find(
1656
- (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
1657
- );
1658
- if (callerLine) {
1659
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1660
- if (match) {
1661
- file = match[1];
1662
- line = parseInt(match[2], 10);
1663
- }
1664
- }
1665
- } catch (e) {
1666
- }
1667
- const trackedHandler = wrappedHandler;
2050
+ const { file, line } = getCallerInfo();
2051
+ const trackingHandler = wrappedHandler;
1668
2052
  wrappedHandler = async (ctx) => {
1669
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1670
- ctx.handlerStack.push({
1671
- name: handler.name || "anonymous",
1672
- file,
1673
- line
1674
- });
2053
+ if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2054
+ return trackingHandler(ctx);
2055
+ }
2056
+ const startTime = performance.now();
2057
+ let error = void 0;
2058
+ try {
2059
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2060
+ ctx.handlerStack.push({
2061
+ name: handler.name || "anonymous",
2062
+ file,
2063
+ line
2064
+ });
2065
+ }
2066
+ return await trackingHandler(ctx);
2067
+ } catch (e) {
2068
+ error = e;
2069
+ throw e;
2070
+ } finally {
2071
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2072
+ const duration = performance.now() - startTime;
2073
+ const config = ctx.app.applicationConfig;
2074
+ try {
2075
+ const timestamp = Date.now();
2076
+ const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2077
+ await datastore.set("middleware_tracking", key, {
2078
+ name: handler.name || "anonymous",
2079
+ path: ctx.path,
2080
+ timestamp,
2081
+ duration,
2082
+ file,
2083
+ line,
2084
+ error: error ? String(error) : void 0,
2085
+ metadata: {
2086
+ isBuiltin: handler.isBuiltin,
2087
+ pluginName: handler.pluginName
2088
+ }
2089
+ });
2090
+ const ttl = config.middlewareTrackingTTL ?? 864e5;
2091
+ const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2092
+ const cutoff = Date.now() - ttl;
2093
+ await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2094
+ const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2095
+ if (results && results[0] && results[0].count > maxCapacity) {
2096
+ const toDelete = results[0].count - maxCapacity;
2097
+ await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2098
+ }
2099
+ } catch (datastoreError) {
2100
+ console.error("Failed to store middleware tracking:", datastoreError);
2101
+ }
2102
+ }
1675
2103
  }
1676
- return trackedHandler(ctx);
1677
2104
  };
1678
- wrappedHandler.originalHandler = trackedHandler.originalHandler || trackedHandler;
2105
+ wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2106
+ let bakedHandler = wrappedHandler;
2107
+ if (this.config?.hooks) {
2108
+ bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2109
+ }
1679
2110
  this[$routes].push({
1680
2111
  method,
1681
2112
  path,
1682
- regex,
1683
- keys,
1684
- handler: wrappedHandler,
2113
+ regex: regex ?? new RegExp(""),
2114
+ keys: keys ?? [],
2115
+ handler,
2116
+ bakedHandler,
1685
2117
  handlerSpec: spec,
1686
2118
  group,
1687
- guards: routeGuards.length > 0 ? routeGuards : void 0,
1688
- requestTimeout: effectiveTimeout
1689
- // Save for inspection? Or just relying on closure
2119
+ hooks: this.config?.hooks,
2120
+ requestTimeout,
2121
+ renderer,
2122
+ metadata: {
2123
+ file,
2124
+ line
2125
+ },
2126
+ controller
1690
2127
  });
2128
+ this.trie.insert(method, path, bakedHandler);
1691
2129
  return this;
1692
2130
  }
1693
2131
  get(path, ...args) {
@@ -1761,10 +2199,10 @@ class ShokupanRouter {
1761
2199
  const config = typeof options === "string" ? { root: options } : options;
1762
2200
  const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
1763
2201
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1764
- serveStatic(null, config, prefix);
2202
+ const handlerMiddleware = serveStatic(config, prefix);
1765
2203
  const routeHandler = async (ctx) => {
1766
- const runner = serveStatic(ctx, config, prefix);
1767
- return runner();
2204
+ return handlerMiddleware(ctx, async () => {
2205
+ });
1768
2206
  };
1769
2207
  let groupName = "Static";
1770
2208
  const segments = normalizedPrefix.split("/").filter(Boolean);
@@ -1825,6 +2263,49 @@ class ShokupanRouter {
1825
2263
  return generateOpenApi(this, options);
1826
2264
  }
1827
2265
  }
2266
+ class SystemCpuMonitor {
2267
+ constructor(intervalMs = 1e3) {
2268
+ this.intervalMs = intervalMs;
2269
+ }
2270
+ interval = null;
2271
+ lastCpus = [];
2272
+ currentUsage = 0;
2273
+ start() {
2274
+ if (this.interval) return;
2275
+ this.lastCpus = os.cpus();
2276
+ this.interval = setInterval(() => this.update(), this.intervalMs);
2277
+ }
2278
+ stop() {
2279
+ if (this.interval) {
2280
+ clearInterval(this.interval);
2281
+ this.interval = null;
2282
+ }
2283
+ }
2284
+ getUsage() {
2285
+ return this.currentUsage;
2286
+ }
2287
+ update() {
2288
+ const cpus = os.cpus();
2289
+ let idle = 0;
2290
+ let total = 0;
2291
+ for (let i = 0; i < cpus.length; i++) {
2292
+ const cpu = cpus[i];
2293
+ const prev = this.lastCpus[i];
2294
+ let type;
2295
+ for (type in cpu.times) {
2296
+ const ticks = cpu.times[type];
2297
+ const prevTicks = prev.times[type];
2298
+ const diff = ticks - prevTicks;
2299
+ total += diff;
2300
+ if (type === "idle") {
2301
+ idle += diff;
2302
+ }
2303
+ }
2304
+ }
2305
+ this.lastCpus = cpus;
2306
+ this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
2307
+ }
2308
+ }
1828
2309
  const defaults = {
1829
2310
  port: 3e3,
1830
2311
  hostname: "localhost",
@@ -1836,51 +2317,67 @@ trace.getTracer("shokupan.application");
1836
2317
  class Shokupan extends ShokupanRouter {
1837
2318
  applicationConfig = {};
1838
2319
  openApiSpec;
1839
- middleware = [];
1840
2320
  composedMiddleware;
2321
+ cpuMonitor;
2322
+ hookCache = /* @__PURE__ */ new Map();
2323
+ hooksInitialized = false;
1841
2324
  get logger() {
1842
2325
  return this.applicationConfig.logger;
1843
2326
  }
1844
2327
  constructor(applicationConfig = {}) {
1845
- super();
2328
+ const config = Object.assign({}, defaults, applicationConfig);
2329
+ const { hooks, ...routerConfig } = config;
2330
+ super(routerConfig);
1846
2331
  this[$isApplication] = true;
1847
2332
  this[$appRoot] = this;
1848
- Object.assign(this.applicationConfig, defaults, applicationConfig);
2333
+ this.applicationConfig = config;
2334
+ const { file, line } = getCallerInfo();
2335
+ this.metadata = {
2336
+ file,
2337
+ line,
2338
+ name: "ShokupanApplication"
2339
+ };
1849
2340
  }
1850
2341
  /**
1851
2342
  * Adds middleware to the application.
1852
2343
  */
1853
2344
  use(middleware) {
1854
2345
  let trackedMiddleware = middleware;
1855
- let file = "unknown";
1856
- let line = 0;
1857
- try {
1858
- const err = new Error();
1859
- const stack = err.stack?.split("\n") || [];
1860
- const callerLine = stack.find(
1861
- (l) => l.includes(":") && !l.includes("shokupan.ts") && !l.includes("router.ts") && // In case called from router?
1862
- !l.includes("node_modules") && !l.includes("bun:main")
1863
- );
1864
- if (callerLine) {
1865
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1866
- if (match) {
1867
- file = match[1];
1868
- line = parseInt(match[2], 10);
1869
- }
1870
- }
1871
- } catch (e) {
2346
+ const { file, line } = getCallerInfo();
2347
+ if (!middleware.metadata) {
2348
+ middleware.metadata = {
2349
+ file,
2350
+ line,
2351
+ name: middleware.name || "middleware",
2352
+ isBuiltin: middleware.isBuiltin,
2353
+ pluginName: middleware.pluginName
2354
+ };
1872
2355
  }
1873
2356
  trackedMiddleware = async (ctx, next) => {
1874
2357
  const c = ctx;
1875
2358
  if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
1876
- c.handlerStack.push({
1877
- name: middleware.name || "middleware",
1878
- file,
1879
- line
1880
- });
2359
+ const metadata = middleware.metadata || {};
2360
+ const start = performance.now();
2361
+ const item = {
2362
+ name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2363
+ file: metadata.file || file,
2364
+ line: metadata.line || line,
2365
+ isBuiltin: metadata.isBuiltin,
2366
+ startTime: start,
2367
+ duration: -1
2368
+ };
2369
+ c.handlerStack.push(item);
2370
+ try {
2371
+ return await middleware(ctx, next);
2372
+ } finally {
2373
+ item.duration = performance.now() - start;
2374
+ }
1881
2375
  }
1882
2376
  return middleware(ctx, next);
1883
2377
  };
2378
+ trackedMiddleware.metadata = middleware.metadata;
2379
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2380
+ trackedMiddleware.order = this.middleware.length;
1884
2381
  this.middleware.push(trackedMiddleware);
1885
2382
  return this;
1886
2383
  }
@@ -1892,6 +2389,15 @@ class Shokupan extends ShokupanRouter {
1892
2389
  this.startupHooks.push(callback);
1893
2390
  return this;
1894
2391
  }
2392
+ specAvailableHooks = [];
2393
+ /**
2394
+ * Registers a callback to be executed when the OpenAPI spec is available.
2395
+ * This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
2396
+ */
2397
+ onSpecAvailable(callback) {
2398
+ this.specAvailableHooks.push(callback);
2399
+ return this;
2400
+ }
1895
2401
  /**
1896
2402
  * Starts the application server.
1897
2403
  *
@@ -1908,8 +2414,15 @@ class Shokupan extends ShokupanRouter {
1908
2414
  }
1909
2415
  if (this.applicationConfig.enableOpenApiGen) {
1910
2416
  this.openApiSpec = await generateOpenApi(this);
2417
+ for (const hook of this.specAvailableHooks) {
2418
+ await hook(this.openApiSpec);
2419
+ }
1911
2420
  }
1912
2421
  if (port === 0 && process.platform === "linux") ;
2422
+ if (this.applicationConfig.autoBackpressureFeedback) {
2423
+ this.cpuMonitor = new SystemCpuMonitor();
2424
+ this.cpuMonitor.start();
2425
+ }
1913
2426
  const serveOptions = {
1914
2427
  port: finalPort,
1915
2428
  hostname: this.applicationConfig.hostname,
@@ -1934,7 +2447,7 @@ class Shokupan extends ShokupanRouter {
1934
2447
  };
1935
2448
  let factory = this.applicationConfig.serverFactory;
1936
2449
  if (!factory && typeof Bun === "undefined") {
1937
- const { createHttpServer } = await import("./server-adapter-CnQFr4P7.js");
2450
+ const { createHttpServer } = await import("./server-adapter-BWrEJbKL.js");
1938
2451
  factory = createHttpServer();
1939
2452
  }
1940
2453
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2019,11 +2532,18 @@ class Shokupan extends ShokupanRouter {
2019
2532
  }
2020
2533
  async handleRequest(req, server) {
2021
2534
  const request = req;
2022
- const ctx = new ShokupanContext(request, server, void 0, this, this.applicationConfig.enableMiddlewareTracking);
2535
+ const controller = new AbortController();
2536
+ const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
2023
2537
  const handle = async () => {
2538
+ if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2539
+ const msg = "Too Many Requests (CPU Backpressure)";
2540
+ const res = ctx.text(msg, 429);
2541
+ await this.executeHook("onResponseEnd", ctx, res);
2542
+ return res;
2543
+ }
2024
2544
  try {
2025
- if (this.applicationConfig.hooks?.onRequestStart) {
2026
- await this.applicationConfig.hooks.onRequestStart(ctx);
2545
+ if (this.hasHook("onRequestStart")) {
2546
+ await this.executeHook("onRequestStart", ctx);
2027
2547
  }
2028
2548
  const fn = this.composedMiddleware ??= compose(this.middleware);
2029
2549
  const result = await fn(ctx, async () => {
@@ -2039,23 +2559,24 @@ class Shokupan extends ShokupanRouter {
2039
2559
  response = result;
2040
2560
  } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2041
2561
  response = ctx._finalResponse;
2042
- } else if ((result === null || result === void 0) && ctx.response.status === 404) {
2043
- const span = asyncContext.getStore()?.get("span");
2044
- if (span) span.setAttribute("http.status_code", 404);
2045
- response = ctx.text("Not Found", 404);
2046
2562
  } else if (result === null || result === void 0) {
2047
- if (ctx._finalResponse) response = ctx._finalResponse;
2048
- else response = ctx.text("Not Found", 404);
2563
+ if (ctx._finalResponse instanceof Response) {
2564
+ response = ctx._finalResponse;
2565
+ } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
2566
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2567
+ } else {
2568
+ response = ctx.text("Not Found", 404);
2569
+ }
2049
2570
  } else if (typeof result === "object") {
2050
2571
  response = ctx.json(result);
2051
2572
  } else {
2052
2573
  response = ctx.text(String(result));
2053
2574
  }
2054
- if (this.applicationConfig.hooks?.onRequestEnd) {
2055
- await this.applicationConfig.hooks.onRequestEnd(ctx);
2575
+ if (this.hasHook("onRequestEnd")) {
2576
+ await this.executeHook("onRequestEnd", ctx);
2056
2577
  }
2057
- if (this.applicationConfig.hooks?.onResponseStart) {
2058
- await this.applicationConfig.hooks.onResponseStart(ctx, response);
2578
+ if (this.hasHook("onResponseStart")) {
2579
+ await this.executeHook("onResponseStart", ctx, response);
2059
2580
  }
2060
2581
  return response;
2061
2582
  } catch (err) {
@@ -2065,28 +2586,21 @@ class Shokupan extends ShokupanRouter {
2065
2586
  const status = err.status || err.statusCode || 500;
2066
2587
  const body = { error: err.message || "Internal Server Error" };
2067
2588
  if (err.errors) body.errors = err.errors;
2068
- if (this.applicationConfig.hooks?.onError) {
2069
- try {
2070
- await this.applicationConfig.hooks.onError(err, ctx);
2071
- } catch (hookErr) {
2072
- console.error("Error in onError hook:", hookErr);
2073
- }
2589
+ if (this.hasHook("onError")) {
2590
+ await this.executeHook("onError", err, ctx);
2074
2591
  }
2075
2592
  return ctx.json(body, status);
2076
2593
  }
2077
2594
  };
2078
2595
  let executionPromise = handle();
2079
2596
  const timeoutMs = this.applicationConfig.requestTimeout;
2080
- if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
2597
+ if (timeoutMs && timeoutMs > 0) {
2081
2598
  let timeoutId;
2082
2599
  const timeoutPromise = new Promise((_, reject) => {
2083
2600
  timeoutId = setTimeout(async () => {
2084
- try {
2085
- if (this.applicationConfig.hooks?.onRequestTimeout) {
2086
- await this.applicationConfig.hooks.onRequestTimeout(ctx);
2087
- }
2088
- } catch (e) {
2089
- console.error("Error in onRequestTimeout hook:", e);
2601
+ controller.abort();
2602
+ if (this.hasHook("onRequestTimeout")) {
2603
+ await this.executeHook("onRequestTimeout", ctx);
2090
2604
  }
2091
2605
  reject(new Error("Request Timeout"));
2092
2606
  }, timeoutMs);
@@ -2100,12 +2614,56 @@ class Shokupan extends ShokupanRouter {
2100
2614
  console.error("Unexpected error in request execution:", err);
2101
2615
  return ctx.text("Internal Server Error", 500);
2102
2616
  }).then(async (res) => {
2103
- if (this.applicationConfig.hooks?.onResponseEnd) {
2104
- await this.applicationConfig.hooks.onResponseEnd(ctx, res);
2617
+ if (this.hasHook("onResponseEnd")) {
2618
+ await this.executeHook("onResponseEnd", ctx, res);
2105
2619
  }
2106
2620
  return res;
2107
2621
  });
2108
2622
  }
2623
+ ensureHooksInitialized() {
2624
+ const hooks = this.applicationConfig.hooks;
2625
+ if (hooks) {
2626
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2627
+ const hookTypes = [
2628
+ "onRequestStart",
2629
+ "onRequestEnd",
2630
+ "onResponseStart",
2631
+ "onResponseEnd",
2632
+ "onError",
2633
+ "beforeValidate",
2634
+ "afterValidate",
2635
+ "onRequestTimeout",
2636
+ "onReadTimeout",
2637
+ "onWriteTimeout"
2638
+ ];
2639
+ for (const type of hookTypes) {
2640
+ const fns = [];
2641
+ for (const h of hookList) {
2642
+ if (h[type]) fns.push(h[type]);
2643
+ }
2644
+ if (fns.length > 0) {
2645
+ this.hookCache.set(type, fns);
2646
+ }
2647
+ }
2648
+ }
2649
+ this.hooksInitialized = true;
2650
+ }
2651
+ async executeHook(name, ...args) {
2652
+ if (!this.hooksInitialized) {
2653
+ this.ensureHooksInitialized();
2654
+ }
2655
+ const fns = this.hookCache.get(name);
2656
+ if (!fns) return;
2657
+ for (const fn of fns) {
2658
+ await fn(...args);
2659
+ }
2660
+ }
2661
+ hasHook(name) {
2662
+ if (!this.hooksInitialized) {
2663
+ this.ensureHooksInitialized();
2664
+ }
2665
+ return this.hookCache.has(name);
2666
+ }
2109
2667
  }
2110
2668
  class AuthPlugin extends ShokupanRouter {
2111
2669
  constructor(authConfig) {
@@ -2310,7 +2868,7 @@ class AuthPlugin extends ShokupanRouter {
2310
2868
  /**
2311
2869
  * Middleware to verify JWT
2312
2870
  */
2313
- middleware() {
2871
+ getMiddleware() {
2314
2872
  return async (ctx, next) => {
2315
2873
  const authHeader = ctx.req.headers.get("Authorization");
2316
2874
  let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
@@ -2331,12 +2889,16 @@ class AuthPlugin extends ShokupanRouter {
2331
2889
  }
2332
2890
  function Compression(options = {}) {
2333
2891
  const threshold = options.threshold ?? 512;
2334
- return async (ctx, next) => {
2892
+ const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
2335
2893
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
2336
2894
  let method = null;
2337
2895
  if (acceptEncoding.includes("br")) method = "br";
2338
- else if (acceptEncoding.includes("zstd")) method = "zstd";
2339
- else if (acceptEncoding.includes("gzip")) method = "gzip";
2896
+ else if (acceptEncoding.includes("zstd")) {
2897
+ if (typeof Bun === "undefined") {
2898
+ throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
2899
+ }
2900
+ method = "zstd";
2901
+ } else if (acceptEncoding.includes("gzip")) method = "gzip";
2340
2902
  else if (acceptEncoding.includes("deflate")) method = "deflate";
2341
2903
  if (!method) return next();
2342
2904
  let response = await next();
@@ -2345,18 +2907,34 @@ function Compression(options = {}) {
2345
2907
  }
2346
2908
  if (response instanceof Response) {
2347
2909
  if (response.headers.has("Content-Encoding")) return response;
2348
- const body = await response.arrayBuffer();
2349
- if (body.byteLength < threshold) {
2910
+ let body;
2911
+ let bodySize;
2912
+ if (ctx._rawBody !== void 0) {
2913
+ if (typeof ctx._rawBody === "string") {
2914
+ const encoded = new TextEncoder().encode(ctx._rawBody);
2915
+ body = encoded;
2916
+ bodySize = encoded.byteLength;
2917
+ } else if (ctx._rawBody instanceof Uint8Array) {
2918
+ body = ctx._rawBody;
2919
+ bodySize = ctx._rawBody.byteLength;
2920
+ } else {
2921
+ body = ctx._rawBody;
2922
+ bodySize = body.byteLength;
2923
+ }
2924
+ } else {
2925
+ body = await response.arrayBuffer();
2926
+ bodySize = body.byteLength;
2927
+ }
2928
+ if (bodySize < threshold) {
2350
2929
  return new Response(body, {
2351
2930
  status: response.status,
2352
2931
  statusText: response.statusText,
2353
- headers: response.headers
2932
+ headers: new Headers(response.headers)
2354
2933
  });
2355
2934
  }
2356
2935
  let compressed;
2357
2936
  switch (method) {
2358
2937
  case "br":
2359
- const zlib = require("node:zlib");
2360
2938
  compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
2361
2939
  params: {
2362
2940
  [zlib.constants.BROTLI_PARAM_QUALITY]: 4
@@ -2367,13 +2945,19 @@ function Compression(options = {}) {
2367
2945
  }));
2368
2946
  break;
2369
2947
  case "gzip":
2370
- compressed = Bun.gzipSync(body);
2948
+ compressed = await new Promise((res, rej) => zlib.gzip(body, (err, data) => {
2949
+ if (err) return rej(err);
2950
+ res(data);
2951
+ }));
2371
2952
  break;
2372
2953
  case "zstd":
2373
2954
  compressed = await Bun.zstdCompress(body);
2374
2955
  break;
2375
2956
  default:
2376
- compressed = Bun.deflateSync(body);
2957
+ compressed = await new Promise((res, rej) => zlib.deflate(body, (err, data) => {
2958
+ if (err) return rej(err);
2959
+ res(data);
2960
+ }));
2377
2961
  break;
2378
2962
  }
2379
2963
  const headers = new Headers(response.headers);
@@ -2387,6 +2971,9 @@ function Compression(options = {}) {
2387
2971
  }
2388
2972
  return response;
2389
2973
  };
2974
+ compressionMiddleware.isBuiltin = true;
2975
+ compressionMiddleware.pluginName = "Compression";
2976
+ return compressionMiddleware;
2390
2977
  }
2391
2978
  function Cors(options = {}) {
2392
2979
  const defaults2 = {
@@ -2396,7 +2983,7 @@ function Cors(options = {}) {
2396
2983
  optionsSuccessStatus: 204
2397
2984
  };
2398
2985
  const opts = { ...defaults2, ...options };
2399
- return async (ctx, next) => {
2986
+ const corsMiddleware = async function CorsMiddleware(ctx, next) {
2400
2987
  const headers = new Headers();
2401
2988
  const origin = ctx.headers.get("origin");
2402
2989
  const set = (k, v) => headers.set(k, v);
@@ -2458,6 +3045,9 @@ function Cors(options = {}) {
2458
3045
  }
2459
3046
  return response;
2460
3047
  };
3048
+ corsMiddleware.isBuiltin = true;
3049
+ corsMiddleware.pluginName = "Cors";
3050
+ return corsMiddleware;
2461
3051
  }
2462
3052
  function useExpress(expressMiddleware) {
2463
3053
  return async (ctx, next) => {
@@ -2625,7 +3215,33 @@ const safelyGetBody = async (ctx) => {
2625
3215
  return {};
2626
3216
  }
2627
3217
  };
3218
+ function getValidator(schema) {
3219
+ if (isZod(schema)) {
3220
+ return (data) => validateZod(schema, data);
3221
+ }
3222
+ if (isTypeBox(schema)) {
3223
+ return (data) => validateTypeBox(schema, data);
3224
+ }
3225
+ if (isAjv(schema)) {
3226
+ return (data) => validateAjv(schema, data);
3227
+ }
3228
+ if (isValibotWrapper(schema)) {
3229
+ return (data) => validateValibotWrapper(schema, data);
3230
+ }
3231
+ if (isClass(schema)) {
3232
+ return (data) => validateClassValidator(schema, data);
3233
+ }
3234
+ if (typeof schema === "function") {
3235
+ return schema;
3236
+ }
3237
+ throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
3238
+ }
2628
3239
  function validate(config) {
3240
+ const validators = {};
3241
+ if (config.params) validators.params = getValidator(config.params);
3242
+ if (config.query) validators.query = getValidator(config.query);
3243
+ if (config.headers) validators.headers = getValidator(config.headers);
3244
+ if (config.body) validators.body = getValidator(config.body);
2629
3245
  return async (ctx, next) => {
2630
3246
  const dataToValidate = {};
2631
3247
  if (config.params) dataToValidate.params = ctx.params;
@@ -2644,21 +3260,21 @@ function validate(config) {
2644
3260
  if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2645
3261
  await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2646
3262
  }
2647
- if (config.params) {
2648
- ctx.params = await runValidation(config.params, ctx.params);
3263
+ if (validators.params) {
3264
+ ctx.params = await validators.params(ctx.params);
2649
3265
  }
2650
3266
  let validQuery;
2651
- if (config.query && queryObj) {
2652
- validQuery = await runValidation(config.query, queryObj);
3267
+ if (validators.query && queryObj) {
3268
+ validQuery = await validators.query(queryObj);
2653
3269
  }
2654
- if (config.headers) {
3270
+ if (validators.headers) {
2655
3271
  const headersObj = Object.fromEntries(ctx.req.headers.entries());
2656
- await runValidation(config.headers, headersObj);
3272
+ await validators.headers(headersObj);
2657
3273
  }
2658
3274
  let validBody;
2659
- if (config.body) {
3275
+ if (validators.body) {
2660
3276
  const b = body ?? await safelyGetBody(ctx);
2661
- validBody = await runValidation(config.body, b);
3277
+ validBody = await validators.body(b);
2662
3278
  const req = ctx.req;
2663
3279
  req._bodyValue = validBody;
2664
3280
  Object.defineProperty(req, "json", {
@@ -2677,36 +3293,6 @@ function validate(config) {
2677
3293
  return next();
2678
3294
  };
2679
3295
  }
2680
- async function runValidation(schema, data) {
2681
- if (isZod(schema)) {
2682
- return validateZod(schema, data);
2683
- }
2684
- if (isTypeBox(schema)) {
2685
- return validateTypeBox(schema, data);
2686
- }
2687
- if (isAjv(schema)) {
2688
- return validateAjv(schema, data);
2689
- }
2690
- if (isValibotWrapper(schema)) {
2691
- return validateValibotWrapper(schema, data);
2692
- }
2693
- if (isClass(schema)) {
2694
- return validateClassValidator(schema, data);
2695
- }
2696
- if (isTypeBox(schema)) {
2697
- return validateTypeBox(schema, data);
2698
- }
2699
- if (isAjv(schema)) {
2700
- return validateAjv(schema, data);
2701
- }
2702
- if (isValibotWrapper(schema)) {
2703
- return validateValibotWrapper(schema, data);
2704
- }
2705
- if (typeof schema === "function") {
2706
- return schema(data);
2707
- }
2708
- throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
2709
- }
2710
3296
  const ajv = new Ajv({ coerceTypes: true, allErrors: true });
2711
3297
  addFormats(ajv);
2712
3298
  const compiledValidators = /* @__PURE__ */ new WeakMap();
@@ -2721,17 +3307,18 @@ function openApiValidator() {
2721
3307
  cache = compileValidators(app.openApiSpec);
2722
3308
  compiledValidators.set(app, cache);
2723
3309
  }
2724
- const method = ctx.req.method.toLowerCase();
2725
3310
  let matchPath;
2726
- if (cache.has(ctx.path)) {
3311
+ let matchParams = {};
3312
+ if (cache.validators.has(ctx.path)) {
2727
3313
  matchPath = ctx.path;
2728
3314
  } else {
2729
- for (const specPath of cache.keys()) {
2730
- const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2731
- const regex = new RegExp(regexStr);
3315
+ for (const [path, { regex, paramNames }] of cache.paths) {
2732
3316
  const match = regex.exec(ctx.path);
2733
3317
  if (match) {
2734
- matchPath = specPath;
3318
+ matchPath = path;
3319
+ paramNames.forEach((name, i) => {
3320
+ matchParams[name] = match[i + 1];
3321
+ });
2735
3322
  break;
2736
3323
  }
2737
3324
  }
@@ -2739,7 +3326,8 @@ function openApiValidator() {
2739
3326
  if (!matchPath) {
2740
3327
  return next();
2741
3328
  }
2742
- const validators = cache.get(matchPath)?.[method];
3329
+ const method = ctx.req.method.toLowerCase();
3330
+ const validators = cache.validators.get(matchPath)?.[method];
2743
3331
  if (!validators) {
2744
3332
  return next();
2745
3333
  }
@@ -2764,21 +3352,7 @@ function openApiValidator() {
2764
3352
  }
2765
3353
  }
2766
3354
  if (validators.params) {
2767
- let params = ctx.params;
2768
- if (Object.keys(params).length === 0 && matchPath) {
2769
- const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
2770
- if (paramNames.length > 0) {
2771
- const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2772
- const regex = new RegExp(regexStr);
2773
- const match = regex.exec(ctx.path);
2774
- if (match) {
2775
- params = {};
2776
- paramNames.forEach((name, i) => {
2777
- params[name] = match[i + 1];
2778
- });
2779
- }
2780
- }
2781
- }
3355
+ const params = { ...matchParams, ...ctx.params };
2782
3356
  const valid = validators.params(params);
2783
3357
  if (!valid && validators.params.errors) {
2784
3358
  errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
@@ -2798,15 +3372,27 @@ function openApiValidator() {
2798
3372
  };
2799
3373
  }
2800
3374
  function compileValidators(spec) {
2801
- const cache = /* @__PURE__ */ new Map();
3375
+ const validators = /* @__PURE__ */ new Map();
3376
+ const paths = /* @__PURE__ */ new Map();
2802
3377
  for (const [path, pathItem] of Object.entries(spec.paths || {})) {
3378
+ if (path.includes("{")) {
3379
+ const paramNames = [];
3380
+ const regexStr = "^" + path.replace(/{([^}]+)}/g, (_, name) => {
3381
+ paramNames.push(name);
3382
+ return "([^/]+)";
3383
+ }) + "$";
3384
+ paths.set(path, {
3385
+ regex: new RegExp(regexStr),
3386
+ paramNames
3387
+ });
3388
+ }
2803
3389
  const pathValidators = {};
2804
3390
  for (const [method, operation] of Object.entries(pathItem)) {
2805
3391
  if (method === "parameters" || method === "summary" || method === "description") continue;
2806
3392
  const oper = operation;
2807
- const validators = {};
3393
+ const opValidators = {};
2808
3394
  if (oper.requestBody?.content?.["application/json"]?.schema) {
2809
- validators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
3395
+ opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
2810
3396
  }
2811
3397
  const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
2812
3398
  const queryProps = {};
@@ -2828,85 +3414,41 @@ function compileValidators(spec) {
2828
3414
  }
2829
3415
  }
2830
3416
  if (Object.keys(queryProps).length > 0) {
2831
- validators.query = ajv.compile({
3417
+ opValidators.query = ajv.compile({
2832
3418
  type: "object",
2833
3419
  properties: queryProps,
2834
3420
  required: queryRequired.length > 0 ? queryRequired : void 0
2835
3421
  });
2836
3422
  }
2837
3423
  if (Object.keys(pathProps).length > 0) {
2838
- validators.params = ajv.compile({
3424
+ opValidators.params = ajv.compile({
2839
3425
  type: "object",
2840
3426
  properties: pathProps,
2841
3427
  required: pathRequired.length > 0 ? pathRequired : void 0
2842
3428
  });
2843
3429
  }
2844
3430
  if (Object.keys(headerProps).length > 0) {
2845
- validators.headers = ajv.compile({
3431
+ opValidators.headers = ajv.compile({
2846
3432
  type: "object",
2847
3433
  properties: headerProps,
2848
3434
  required: headerRequired.length > 0 ? headerRequired : void 0
2849
3435
  });
2850
3436
  }
2851
- pathValidators[method] = validators;
3437
+ pathValidators[method] = opValidators;
2852
3438
  }
2853
- cache.set(path, pathValidators);
3439
+ validators.set(path, pathValidators);
2854
3440
  }
2855
- return cache;
3441
+ return { paths, validators };
2856
3442
  }
2857
- function RateLimit(options = {}) {
2858
- const windowMs = options.windowMs || 60 * 1e3;
2859
- const max = options.max || 5;
2860
- const message = options.message || "Too many requests, please try again later.";
2861
- const statusCode = options.statusCode || 429;
2862
- const headers = options.headers !== false;
2863
- const keyGenerator = options.keyGenerator || ((ctx) => {
2864
- return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
3443
+ function precompileValidators(app, spec) {
3444
+ const cache = compileValidators(spec);
3445
+ compiledValidators.set(app, cache);
3446
+ }
3447
+ function enableOpenApiValidation(app) {
3448
+ app.use(openApiValidator());
3449
+ app.onSpecAvailable((spec) => {
3450
+ precompileValidators(app, spec);
2865
3451
  });
2866
- const skip = options.skip || (() => false);
2867
- const hits = /* @__PURE__ */ new Map();
2868
- const interval = setInterval(() => {
2869
- const now = Date.now();
2870
- for (const [key, record] of hits.entries()) {
2871
- if (record.resetTime <= now) {
2872
- hits.delete(key);
2873
- }
2874
- }
2875
- }, windowMs);
2876
- interval.unref?.();
2877
- return async (ctx, next) => {
2878
- if (skip(ctx)) return next();
2879
- const key = keyGenerator(ctx);
2880
- const now = Date.now();
2881
- let record = hits.get(key);
2882
- if (!record || record.resetTime <= now) {
2883
- record = {
2884
- hits: 0,
2885
- resetTime: now + windowMs
2886
- };
2887
- hits.set(key, record);
2888
- }
2889
- record.hits++;
2890
- const remaining = Math.max(0, max - record.hits);
2891
- const resetTime = Math.ceil(record.resetTime / 1e3);
2892
- if (record.hits > max) {
2893
- if (headers) {
2894
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2895
- res.headers.set("X-RateLimit-Limit", String(max));
2896
- res.headers.set("X-RateLimit-Remaining", "0");
2897
- res.headers.set("X-RateLimit-Reset", String(resetTime));
2898
- return res;
2899
- }
2900
- return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2901
- }
2902
- const response = await next();
2903
- if (response instanceof Response && headers) {
2904
- response.headers.set("X-RateLimit-Limit", String(max));
2905
- response.headers.set("X-RateLimit-Remaining", String(remaining));
2906
- response.headers.set("X-RateLimit-Reset", String(resetTime));
2907
- }
2908
- return response;
2909
- };
2910
3452
  }
2911
3453
  const eta = new Eta();
2912
3454
  class ScalarPlugin extends ShokupanRouter {
@@ -2983,7 +3525,7 @@ class ScalarPlugin extends ShokupanRouter {
2983
3525
  }
2984
3526
  }
2985
3527
  function SecurityHeaders(options = {}) {
2986
- return async (ctx, next) => {
3528
+ const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
2987
3529
  const headers = {};
2988
3530
  const set = (k, v) => headers[k] = v;
2989
3531
  if (options.dnsPrefetchControl !== false) {
@@ -3037,6 +3579,9 @@ function SecurityHeaders(options = {}) {
3037
3579
  }
3038
3580
  return response;
3039
3581
  };
3582
+ securityHeadersMiddleware.isBuiltin = true;
3583
+ securityHeadersMiddleware.pluginName = "SecurityHeaders";
3584
+ return securityHeadersMiddleware;
3040
3585
  }
3041
3586
  class Cookie {
3042
3587
  maxAge;
@@ -3150,7 +3695,7 @@ function Session(options) {
3150
3695
  const resave = options.resave === void 0 ? true : options.resave;
3151
3696
  const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
3152
3697
  const rolling = options.rolling || false;
3153
- return async (ctx, next) => {
3698
+ const sessionMiddleware = async function SessionMiddleware(ctx, next) {
3154
3699
  let reqSessionId = null;
3155
3700
  const cookieHeader = ctx.req.headers.get("cookie");
3156
3701
  const cookies = {};
@@ -3286,6 +3831,9 @@ function Session(options) {
3286
3831
  }
3287
3832
  return result;
3288
3833
  };
3834
+ sessionMiddleware.isBuiltin = true;
3835
+ sessionMiddleware.pluginName = "Session";
3836
+ return sessionMiddleware;
3289
3837
  }
3290
3838
  export {
3291
3839
  $appRoot,
@@ -3326,6 +3874,7 @@ export {
3326
3874
  Put,
3327
3875
  Query,
3328
3876
  RateLimit,
3877
+ RateLimitMiddleware,
3329
3878
  Req,
3330
3879
  RouteParamType,
3331
3880
  RouterRegistry,
@@ -3341,8 +3890,11 @@ export {
3341
3890
  Spec,
3342
3891
  Use,
3343
3892
  ValidationError,
3893
+ compileValidators,
3344
3894
  compose,
3895
+ enableOpenApiValidation,
3345
3896
  openApiValidator,
3897
+ precompileValidators,
3346
3898
  useExpress,
3347
3899
  valibot,
3348
3900
  validate