shokupan 0.1.0 → 0.2.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.
package/dist/index.cjs CHANGED
@@ -1,17 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const api = require("@opentelemetry/api");
4
3
  const eta$2 = require("eta");
5
4
  const promises = require("fs/promises");
6
5
  const path = require("path");
7
6
  const node_async_hooks = require("node:async_hooks");
7
+ const api = require("@opentelemetry/api");
8
8
  const arctic = require("arctic");
9
9
  const jose = require("jose");
10
- const openapiAnalyzer = require("./openapi-analyzer-CFqgSLNK.cjs");
11
- const crypto = require("crypto");
12
- const events = require("events");
10
+ const Ajv = require("ajv");
11
+ const addFormats = require("ajv-formats");
13
12
  const classTransformer = require("class-transformer");
14
13
  const classValidator = require("class-validator");
14
+ const openapiAnalyzer = require("./openapi-analyzer-BN0wFCML.cjs");
15
+ const crypto = require("crypto");
16
+ const events = require("events");
15
17
  function _interopNamespaceDefault(e) {
16
18
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
17
19
  if (e) {
@@ -30,12 +32,13 @@ function _interopNamespaceDefault(e) {
30
32
  }
31
33
  const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
32
34
  class ShokupanResponse {
33
- _headers = new Headers();
35
+ _headers = null;
34
36
  _status = 200;
35
37
  /**
36
38
  * Get the current headers
37
39
  */
38
40
  get headers() {
41
+ if (!this._headers) this._headers = new Headers();
39
42
  return this._headers;
40
43
  }
41
44
  /**
@@ -56,6 +59,7 @@ class ShokupanResponse {
56
59
  * @param value Header value
57
60
  */
58
61
  set(key, value) {
62
+ if (!this._headers) this._headers = new Headers();
59
63
  this._headers.set(key, value);
60
64
  return this;
61
65
  }
@@ -65,6 +69,7 @@ class ShokupanResponse {
65
69
  * @param value Header value
66
70
  */
67
71
  append(key, value) {
72
+ if (!this._headers) this._headers = new Headers();
68
73
  this._headers.append(key, value);
69
74
  return this;
70
75
  }
@@ -73,29 +78,58 @@ class ShokupanResponse {
73
78
  * @param key Header name
74
79
  */
75
80
  get(key) {
76
- return this._headers.get(key);
81
+ return this._headers?.get(key) || null;
77
82
  }
78
83
  /**
79
84
  * Check if a header exists
80
85
  * @param key Header name
81
86
  */
82
87
  has(key) {
83
- return this._headers.has(key);
88
+ return this._headers?.has(key) || false;
89
+ }
90
+ /**
91
+ * Internal: check if headers have been initialized/modified
92
+ */
93
+ get hasPopulatedHeaders() {
94
+ return this._headers !== null;
84
95
  }
85
96
  }
86
97
  class ShokupanContext {
87
- constructor(request, server, state, app) {
98
+ constructor(request, server, state, app, enableMiddlewareTracking = false) {
88
99
  this.request = request;
89
100
  this.server = server;
90
101
  this.app = app;
91
- this.url = new URL(request.url);
92
102
  this.state = state || {};
103
+ if (enableMiddlewareTracking) {
104
+ const self = this;
105
+ this.state = new Proxy(this.state, {
106
+ set(target, p, newValue, receiver) {
107
+ const result = Reflect.set(target, p, newValue, receiver);
108
+ const currentHandler = self.handlerStack[self.handlerStack.length - 1];
109
+ if (currentHandler) {
110
+ if (!currentHandler.stateChanges) currentHandler.stateChanges = {};
111
+ currentHandler.stateChanges[p] = newValue;
112
+ }
113
+ return result;
114
+ }
115
+ });
116
+ }
93
117
  this.response = new ShokupanResponse();
94
118
  }
95
- url;
119
+ _url;
96
120
  params = {};
121
+ // Router assigns this, but default to empty object
97
122
  state;
123
+ handlerStack = [];
98
124
  response;
125
+ _finalResponse;
126
+ get url() {
127
+ if (!this._url) {
128
+ const urlString = this.request.url || "http://localhost/";
129
+ this._url = new URL(urlString);
130
+ }
131
+ return this._url;
132
+ }
99
133
  /**
100
134
  * Base request
101
135
  */
@@ -112,7 +146,26 @@ class ShokupanContext {
112
146
  * Request path
113
147
  */
114
148
  get path() {
115
- return this.url.pathname;
149
+ if (this._url) return this._url.pathname;
150
+ const url = this.request.url;
151
+ let queryIndex = url.indexOf("?");
152
+ const end = queryIndex === -1 ? url.length : queryIndex;
153
+ let start = 0;
154
+ const protocolIndex = url.indexOf("://");
155
+ if (protocolIndex !== -1) {
156
+ const hostStart = protocolIndex + 3;
157
+ const pathStart = url.indexOf("/", hostStart);
158
+ if (pathStart !== -1 && pathStart < end) {
159
+ start = pathStart;
160
+ } else {
161
+ return "/";
162
+ }
163
+ } else {
164
+ if (url.charCodeAt(0) === 47) {
165
+ start = 0;
166
+ }
167
+ }
168
+ return url.substring(start, end);
116
169
  }
117
170
  /**
118
171
  * Request query params
@@ -209,9 +262,25 @@ class ShokupanContext {
209
262
  return this;
210
263
  }
211
264
  mergeHeaders(headers) {
212
- const h = new Headers(this.response.headers);
265
+ let h;
266
+ if (this.response.hasPopulatedHeaders) {
267
+ h = new Headers(this.response.headers);
268
+ } else {
269
+ h = new Headers();
270
+ }
213
271
  if (headers) {
214
- new Headers(headers).forEach((v, k) => h.set(k, v));
272
+ if (headers instanceof Headers) {
273
+ headers.forEach((v, k) => h.set(k, v));
274
+ } else if (Array.isArray(headers)) {
275
+ headers.forEach(([k, v]) => h.set(k, v));
276
+ } else {
277
+ const keys = Object.keys(headers);
278
+ for (let i = 0; i < keys.length; i++) {
279
+ const key = keys[i];
280
+ const val = headers[key];
281
+ h.set(key, val);
282
+ }
283
+ }
215
284
  }
216
285
  return h;
217
286
  }
@@ -224,7 +293,8 @@ class ShokupanContext {
224
293
  send(body, options) {
225
294
  const headers = this.mergeHeaders(options?.headers);
226
295
  const status = options?.status ?? this.response.status;
227
- return new Response(body, { status, headers });
296
+ this._finalResponse = new Response(body, { status, headers });
297
+ return this._finalResponse;
228
298
  }
229
299
  /**
230
300
  * Read request body
@@ -243,19 +313,36 @@ class ShokupanContext {
243
313
  * Respond with a JSON object
244
314
  */
245
315
  json(data, status, headers) {
316
+ const finalStatus = status ?? this.response.status;
317
+ const jsonString = JSON.stringify(data);
318
+ if (!headers && !this.response.hasPopulatedHeaders) {
319
+ this._finalResponse = new Response(jsonString, {
320
+ status: finalStatus,
321
+ headers: { "content-type": "application/json" }
322
+ });
323
+ return this._finalResponse;
324
+ }
246
325
  const finalHeaders = this.mergeHeaders(headers);
247
326
  finalHeaders.set("content-type", "application/json");
248
- const finalStatus = status ?? this.response.status;
249
- return new Response(JSON.stringify(data), { status: finalStatus, headers: finalHeaders });
327
+ this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
328
+ return this._finalResponse;
250
329
  }
251
330
  /**
252
331
  * Respond with a text string
253
332
  */
254
333
  text(data, status, headers) {
334
+ const finalStatus = status ?? this.response.status;
335
+ if (!headers && !this.response.hasPopulatedHeaders) {
336
+ this._finalResponse = new Response(data, {
337
+ status: finalStatus,
338
+ headers: { "content-type": "text/plain" }
339
+ });
340
+ return this._finalResponse;
341
+ }
255
342
  const finalHeaders = this.mergeHeaders(headers);
256
343
  finalHeaders.set("content-type", "text/plain");
257
- const finalStatus = status ?? this.response.status;
258
- return new Response(data, { status: finalStatus, headers: finalHeaders });
344
+ this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
345
+ return this._finalResponse;
259
346
  }
260
347
  /**
261
348
  * Respond with HTML content
@@ -264,7 +351,8 @@ class ShokupanContext {
264
351
  const finalHeaders = this.mergeHeaders(headers);
265
352
  finalHeaders.set("content-type", "text/html");
266
353
  const finalStatus = status ?? this.response.status;
267
- return new Response(html, { status: finalStatus, headers: finalHeaders });
354
+ this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
355
+ return this._finalResponse;
268
356
  }
269
357
  /**
270
358
  * Respond with a redirect
@@ -272,7 +360,8 @@ class ShokupanContext {
272
360
  redirect(url, status = 302) {
273
361
  const headers = this.mergeHeaders();
274
362
  headers.set("Location", url);
275
- return new Response(null, { status, headers });
363
+ this._finalResponse = new Response(null, { status, headers });
364
+ return this._finalResponse;
276
365
  }
277
366
  /**
278
367
  * Respond with a status code
@@ -280,7 +369,8 @@ class ShokupanContext {
280
369
  */
281
370
  status(status) {
282
371
  const headers = this.mergeHeaders();
283
- return new Response(null, { status, headers });
372
+ this._finalResponse = new Response(null, { status, headers });
373
+ return this._finalResponse;
284
374
  }
285
375
  /**
286
376
  * Respond with a file
@@ -288,7 +378,8 @@ class ShokupanContext {
288
378
  file(path2, fileOptions, responseOptions) {
289
379
  const headers = this.mergeHeaders(responseOptions?.headers);
290
380
  const status = responseOptions?.status ?? this.response.status;
291
- return new Response(Bun.file(path2, fileOptions), { status, headers });
381
+ this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
382
+ return this._finalResponse;
292
383
  }
293
384
  /**
294
385
  * JSX Rendering Function
@@ -437,69 +528,29 @@ function Inject(token) {
437
528
  });
438
529
  };
439
530
  }
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"
531
+ const compose = (middleware) => {
532
+ if (!middleware.length) {
533
+ return (context, next) => {
534
+ return next ? next() : Promise.resolve();
535
+ };
536
+ }
537
+ return function dispatch(context, next) {
538
+ let index = -1;
539
+ function runner(i) {
540
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
541
+ index = i;
542
+ if (i >= middleware.length) {
543
+ return next ? next() : Promise.resolve();
471
544
  }
472
- }, async (span) => {
545
+ const fn = middleware[i];
473
546
  try {
474
- const result = await fn.apply(this, args);
475
- return result;
547
+ return Promise.resolve(fn(context, () => runner(i + 1)));
476
548
  } catch (err) {
477
- span.recordException(err);
478
- span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
479
- throw err;
480
- } finally {
481
- span.end();
549
+ return Promise.reject(err);
482
550
  }
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
551
  }
500
- return runner();
501
- }
502
- return fn;
552
+ return runner(0);
553
+ };
503
554
  };
504
555
  class ShokupanRequestBase {
505
556
  method;
@@ -726,7 +777,7 @@ async function generateOpenApi(rootRouter, options = {}) {
726
777
  const defaultTagName = options.defaultTag || "Application";
727
778
  let astRoutes = [];
728
779
  try {
729
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-CFqgSLNK.cjs"));
780
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-BN0wFCML.cjs"));
730
781
  const analyzer = new OpenAPIAnalyzer(process.cwd());
731
782
  const { applications } = await analyzer.analyze();
732
783
  const appMap = /* @__PURE__ */ new Map();
@@ -1126,6 +1177,29 @@ function serveStatic(ctx, config, prefix) {
1126
1177
  };
1127
1178
  }
1128
1179
  const asyncContext = new node_async_hooks.AsyncLocalStorage();
1180
+ const tracer = api.trace.getTracer("shokupan.middleware");
1181
+ function traceHandler(fn, name) {
1182
+ return async function(...args) {
1183
+ return tracer.startActiveSpan(`route handler - ${name}`, {
1184
+ kind: api.SpanKind.INTERNAL,
1185
+ attributes: {
1186
+ "http.route": name,
1187
+ "component": "shokupan.route"
1188
+ }
1189
+ }, async (span) => {
1190
+ try {
1191
+ const result = await fn.apply(this, args);
1192
+ return result;
1193
+ } catch (err) {
1194
+ span.recordException(err);
1195
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
1196
+ throw err;
1197
+ } finally {
1198
+ span.end();
1199
+ }
1200
+ });
1201
+ };
1202
+ }
1129
1203
  const RouterRegistry = /* @__PURE__ */ new Map();
1130
1204
  const ShokupanApplicationTree = {};
1131
1205
  class ShokupanRouter {
@@ -1316,7 +1390,7 @@ class ShokupanRouter {
1316
1390
  }
1317
1391
  }
1318
1392
  }
1319
- const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
1393
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1320
1394
  return tracedOriginalHandler.apply(instance, args);
1321
1395
  };
1322
1396
  let finalHandler = wrappedHandler;
@@ -1452,6 +1526,7 @@ class ShokupanRouter {
1452
1526
  applyHooks(match) {
1453
1527
  if (!this.config?.hooks) return match;
1454
1528
  const hooks = this.config.hooks;
1529
+ if (!hooks.onRequestStart && !hooks.onRequestEnd && !hooks.onError) return match;
1455
1530
  const originalHandler = match.handler;
1456
1531
  match.handler = async (ctx) => {
1457
1532
  if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
@@ -1480,16 +1555,25 @@ class ShokupanRouter {
1480
1555
  * @returns Route handler and parameters if found, otherwise null
1481
1556
  */
1482
1557
  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 });
1558
+ const findInRoutes = (routes, m) => {
1559
+ for (const route of routes) {
1560
+ if (route.method !== "ALL" && route.method !== m) continue;
1561
+ const match = route.regex.exec(path2);
1562
+ if (match) {
1563
+ const params = {};
1564
+ route.keys.forEach((key, index) => {
1565
+ params[key] = match[index + 1];
1566
+ });
1567
+ return this.applyHooks({ handler: route.handler, params });
1568
+ }
1492
1569
  }
1570
+ return null;
1571
+ };
1572
+ let result = findInRoutes(this[$routes], method);
1573
+ if (result) return result;
1574
+ if (method === "HEAD") {
1575
+ result = findInRoutes(this[$routes], "GET");
1576
+ if (result) return result;
1493
1577
  }
1494
1578
  for (const child of this[$childRouters]) {
1495
1579
  const prefix = child[$mountPath];
@@ -1582,6 +1666,35 @@ class ShokupanRouter {
1582
1666
  return innerHandler(ctx);
1583
1667
  };
1584
1668
  }
1669
+ let file = "unknown";
1670
+ let line = 0;
1671
+ try {
1672
+ const err = new Error();
1673
+ const stack = err.stack?.split("\n") || [];
1674
+ const callerLine = stack.find(
1675
+ (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
1676
+ );
1677
+ if (callerLine) {
1678
+ const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1679
+ if (match) {
1680
+ file = match[1];
1681
+ line = parseInt(match[2], 10);
1682
+ }
1683
+ }
1684
+ } catch (e) {
1685
+ }
1686
+ const trackedHandler = wrappedHandler;
1687
+ wrappedHandler = async (ctx) => {
1688
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1689
+ ctx.handlerStack.push({
1690
+ name: handler.name || "anonymous",
1691
+ file,
1692
+ line
1693
+ });
1694
+ }
1695
+ return trackedHandler(ctx);
1696
+ };
1697
+ wrappedHandler.originalHandler = trackedHandler.originalHandler || trackedHandler;
1585
1698
  this[$routes].push({
1586
1699
  method,
1587
1700
  path: path2,
@@ -1627,7 +1740,35 @@ class ShokupanRouter {
1627
1740
  guard(specOrHandler, handler) {
1628
1741
  const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
1629
1742
  const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
1630
- this.currentGuards.push({ handler: guardHandler, spec });
1743
+ let file = "unknown";
1744
+ let line = 0;
1745
+ try {
1746
+ const err = new Error();
1747
+ const stack = err.stack?.split("\n") || [];
1748
+ const callerLine = stack.find(
1749
+ (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
1750
+ );
1751
+ if (callerLine) {
1752
+ const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1753
+ if (match) {
1754
+ file = match[1];
1755
+ line = parseInt(match[2], 10);
1756
+ }
1757
+ }
1758
+ } catch (e) {
1759
+ }
1760
+ const trackedGuard = async (ctx, next) => {
1761
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1762
+ ctx.handlerStack.push({
1763
+ name: guardHandler.name || "guard",
1764
+ file,
1765
+ line
1766
+ });
1767
+ }
1768
+ return guardHandler(ctx, next);
1769
+ };
1770
+ trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
1771
+ this.currentGuards.push({ handler: trackedGuard, spec });
1631
1772
  return this;
1632
1773
  }
1633
1774
  /**
@@ -1715,6 +1856,7 @@ class Shokupan extends ShokupanRouter {
1715
1856
  applicationConfig = {};
1716
1857
  openApiSpec;
1717
1858
  middleware = [];
1859
+ composedMiddleware;
1718
1860
  get logger() {
1719
1861
  return this.applicationConfig.logger;
1720
1862
  }
@@ -1728,7 +1870,37 @@ class Shokupan extends ShokupanRouter {
1728
1870
  * Adds middleware to the application.
1729
1871
  */
1730
1872
  use(middleware) {
1731
- this.middleware.push(middleware);
1873
+ let trackedMiddleware = middleware;
1874
+ let file = "unknown";
1875
+ let line = 0;
1876
+ try {
1877
+ const err = new Error();
1878
+ const stack = err.stack?.split("\n") || [];
1879
+ const callerLine = stack.find(
1880
+ (l) => l.includes(":") && !l.includes("shokupan.ts") && !l.includes("router.ts") && // In case called from router?
1881
+ !l.includes("node_modules") && !l.includes("bun:main")
1882
+ );
1883
+ if (callerLine) {
1884
+ const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1885
+ if (match) {
1886
+ file = match[1];
1887
+ line = parseInt(match[2], 10);
1888
+ }
1889
+ }
1890
+ } catch (e) {
1891
+ }
1892
+ trackedMiddleware = async (ctx, next) => {
1893
+ const c = ctx;
1894
+ if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
1895
+ c.handlerStack.push({
1896
+ name: middleware.name || "middleware",
1897
+ file,
1898
+ line
1899
+ });
1900
+ }
1901
+ return middleware(ctx, next);
1902
+ };
1903
+ this.middleware.push(trackedMiddleware);
1732
1904
  return this;
1733
1905
  }
1734
1906
  startupHooks = [];
@@ -1763,9 +1935,28 @@ class Shokupan extends ShokupanRouter {
1763
1935
  development: this.applicationConfig.development,
1764
1936
  fetch: this.fetch.bind(this),
1765
1937
  reusePort: this.applicationConfig.reusePort,
1766
- idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0
1938
+ idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
1939
+ websocket: {
1940
+ open(ws) {
1941
+ ws.data?.handler?.open?.(ws);
1942
+ },
1943
+ message(ws, message) {
1944
+ ws.data?.handler?.message?.(ws, message);
1945
+ },
1946
+ drain(ws) {
1947
+ ws.data?.handler?.drain?.(ws);
1948
+ },
1949
+ close(ws, code, reason) {
1950
+ ws.data?.handler?.close?.(ws, code, reason);
1951
+ }
1952
+ }
1767
1953
  };
1768
- const server = this.applicationConfig.serverFactory ? await this.applicationConfig.serverFactory(serveOptions) : Bun.serve(serveOptions);
1954
+ let factory = this.applicationConfig.serverFactory;
1955
+ if (!factory && typeof Bun === "undefined") {
1956
+ const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-BD6oKEto.cjs"));
1957
+ factory = createHttpServer();
1958
+ }
1959
+ const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
1769
1960
  console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
1770
1961
  return server;
1771
1962
  }
@@ -1820,108 +2011,118 @@ class Shokupan extends ShokupanRouter {
1820
2011
  * @returns The response to send.
1821
2012
  */
1822
2013
  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) => {
2014
+ if (this.applicationConfig.enableTracing) {
2015
+ const tracer2 = api.trace.getTracer("shokupan.application");
2016
+ const store = asyncContext.getStore();
2017
+ const attrs = {
2018
+ attributes: {
2019
+ "http.url": req.url,
2020
+ "http.method": req.method
2021
+ }
2022
+ };
2023
+ const parent = store?.get("span");
2024
+ const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
2025
+ return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2026
+ const ctxMap = /* @__PURE__ */ new Map();
2027
+ ctxMap.set("span", span);
2028
+ ctxMap.set("request", req);
2029
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
2030
+ });
2031
+ }
2032
+ if (this.applicationConfig.enableAsyncLocalStorage) {
1834
2033
  const ctxMap = /* @__PURE__ */ new Map();
1835
- ctxMap.set("span", span);
1836
2034
  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);
2035
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
2036
+ }
2037
+ return this.handleRequest(req, server);
2038
+ }
2039
+ async handleRequest(req, server) {
2040
+ const request = req;
2041
+ const ctx = new ShokupanContext(request, server, void 0, this, this.applicationConfig.enableMiddlewareTracking);
2042
+ const handle = async () => {
2043
+ try {
2044
+ if (this.applicationConfig.hooks?.onRequestStart) {
2045
+ await this.applicationConfig.hooks.onRequestStart(ctx);
2046
+ }
2047
+ const fn = this.composedMiddleware ??= compose(this.middleware);
2048
+ const result = await fn(ctx, async () => {
2049
+ const match = this.find(req.method, ctx.path);
2050
+ if (match) {
2051
+ ctx.params = match.params;
2052
+ return match.handler(ctx);
1887
2053
  }
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));
2054
+ return null;
2055
+ });
2056
+ let response;
2057
+ if (result instanceof Response) {
2058
+ response = result;
2059
+ } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2060
+ response = ctx._finalResponse;
2061
+ } else if ((result === null || result === void 0) && ctx.response.status === 404) {
2062
+ const span = asyncContext.getStore()?.get("span");
2063
+ if (span) span.setAttribute("http.status_code", 404);
2064
+ response = ctx.text("Not Found", 404);
2065
+ } else if (result === null || result === void 0) {
2066
+ if (ctx._finalResponse) response = ctx._finalResponse;
2067
+ else response = ctx.text("Not Found", 404);
2068
+ } else if (typeof result === "object") {
2069
+ response = ctx.json(result);
2070
+ } else {
2071
+ response = ctx.text(String(result));
2072
+ }
2073
+ if (this.applicationConfig.hooks?.onRequestEnd) {
2074
+ await this.applicationConfig.hooks.onRequestEnd(ctx);
1906
2075
  }
1907
- return executionPromise.catch((err) => {
1908
- if (err.message === "Request Timeout") {
1909
- return ctx2.text("Request Timeout", 408);
2076
+ if (this.applicationConfig.hooks?.onResponseStart) {
2077
+ await this.applicationConfig.hooks.onResponseStart(ctx, response);
2078
+ }
2079
+ return response;
2080
+ } catch (err) {
2081
+ console.error(err);
2082
+ const span = asyncContext.getStore()?.get("span");
2083
+ if (span) span.setStatus({ code: 2 });
2084
+ const status = err.status || err.statusCode || 500;
2085
+ const body = { error: err.message || "Internal Server Error" };
2086
+ if (err.errors) body.errors = err.errors;
2087
+ if (this.applicationConfig.hooks?.onError) {
2088
+ try {
2089
+ await this.applicationConfig.hooks.onError(err, ctx);
2090
+ } catch (hookErr) {
2091
+ console.error("Error in onError hook:", hookErr);
1910
2092
  }
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);
2093
+ }
2094
+ return ctx.json(body, status);
2095
+ }
2096
+ };
2097
+ let executionPromise = handle();
2098
+ const timeoutMs = this.applicationConfig.requestTimeout;
2099
+ if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
2100
+ let timeoutId;
2101
+ const timeoutPromise = new Promise((_, reject) => {
2102
+ timeoutId = setTimeout(async () => {
2103
+ try {
2104
+ if (this.applicationConfig.hooks?.onRequestTimeout) {
2105
+ await this.applicationConfig.hooks.onRequestTimeout(ctx);
2106
+ }
2107
+ } catch (e) {
2108
+ console.error("Error in onRequestTimeout hook:", e);
1916
2109
  }
1917
- return res;
1918
- }).finally(() => span.end());
1919
- };
1920
- if (this.applicationConfig.enableAsyncLocalStorage) {
1921
- return asyncContext.run(ctxMap, runCallback);
1922
- } else {
1923
- return runCallback();
2110
+ reject(new Error("Request Timeout"));
2111
+ }, timeoutMs);
2112
+ });
2113
+ executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2114
+ }
2115
+ return executionPromise.catch((err) => {
2116
+ if (err.message === "Request Timeout") {
2117
+ return ctx.text("Request Timeout", 408);
2118
+ }
2119
+ console.error("Unexpected error in request execution:", err);
2120
+ return ctx.text("Internal Server Error", 500);
2121
+ }).then(async (res) => {
2122
+ if (this.applicationConfig.hooks?.onResponseEnd) {
2123
+ await this.applicationConfig.hooks.onResponseEnd(ctx, res);
1924
2124
  }
2125
+ return res;
1925
2126
  });
1926
2127
  }
1927
2128
  }
@@ -2148,15 +2349,19 @@ class AuthPlugin extends ShokupanRouter {
2148
2349
  }
2149
2350
  }
2150
2351
  function Compression(options = {}) {
2151
- const threshold = options.threshold ?? 1024;
2352
+ const threshold = options.threshold ?? 512;
2152
2353
  return async (ctx, next) => {
2153
2354
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
2154
2355
  let method = null;
2155
2356
  if (acceptEncoding.includes("br")) method = "br";
2357
+ else if (acceptEncoding.includes("zstd")) method = "zstd";
2156
2358
  else if (acceptEncoding.includes("gzip")) method = "gzip";
2157
2359
  else if (acceptEncoding.includes("deflate")) method = "deflate";
2158
2360
  if (!method) return next();
2159
- const response = await next();
2361
+ let response = await next();
2362
+ if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
2363
+ response = ctx._finalResponse;
2364
+ }
2160
2365
  if (response instanceof Response) {
2161
2366
  if (response.headers.has("Content-Encoding")) return response;
2162
2367
  const body = await response.arrayBuffer();
@@ -2168,17 +2373,31 @@ function Compression(options = {}) {
2168
2373
  });
2169
2374
  }
2170
2375
  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);
2376
+ switch (method) {
2377
+ case "br":
2378
+ const zlib = require("node:zlib");
2379
+ compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
2380
+ params: {
2381
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 4
2382
+ }
2383
+ }, (err, data) => {
2384
+ if (err) return rej(err);
2385
+ res(data);
2386
+ }));
2387
+ break;
2388
+ case "gzip":
2389
+ compressed = Bun.gzipSync(body);
2390
+ break;
2391
+ case "zstd":
2392
+ compressed = await Bun.zstdCompress(body);
2393
+ break;
2394
+ default:
2395
+ compressed = Bun.deflateSync(body);
2396
+ break;
2177
2397
  }
2178
2398
  const headers = new Headers(response.headers);
2179
2399
  headers.set("Content-Encoding", method);
2180
2400
  headers.set("Content-Length", String(compressed.length));
2181
- headers.delete("Content-Length");
2182
2401
  return new Response(compressed, {
2183
2402
  status: response.status,
2184
2403
  statusText: response.statusText,
@@ -2319,72 +2538,407 @@ function useExpress(expressMiddleware) {
2319
2538
  });
2320
2539
  };
2321
2540
  }
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
- };
2375
- }
2376
- const eta = new eta$2.Eta();
2377
- class ScalarPlugin extends ShokupanRouter {
2378
- constructor(pluginOptions) {
2379
- super();
2380
- this.pluginOptions = pluginOptions;
2381
- this.init();
2541
+ class ValidationError extends Error {
2542
+ constructor(errors) {
2543
+ super("Validation Error");
2544
+ this.errors = errors;
2382
2545
  }
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>
2546
+ status = 400;
2547
+ }
2548
+ function isZod(schema) {
2549
+ return typeof schema?.safeParse === "function";
2550
+ }
2551
+ async function validateZod(schema, data) {
2552
+ const result = await schema.safeParseAsync(data);
2553
+ if (!result.success) {
2554
+ throw new ValidationError(result.error.errors);
2555
+ }
2556
+ return result.data;
2557
+ }
2558
+ function isTypeBox(schema) {
2559
+ return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
2560
+ }
2561
+ function validateTypeBox(schema, data) {
2562
+ if (!schema.Check(data)) {
2563
+ throw new ValidationError([...schema.Errors(data)]);
2564
+ }
2565
+ return data;
2566
+ }
2567
+ function isAjv(schema) {
2568
+ return typeof schema === "function" && "errors" in schema;
2569
+ }
2570
+ function validateAjv(schema, data) {
2571
+ const valid = schema(data);
2572
+ if (!valid) {
2573
+ throw new ValidationError(schema.errors);
2574
+ }
2575
+ return data;
2576
+ }
2577
+ const valibot = (schema, parser) => {
2578
+ return {
2579
+ _valibot: true,
2580
+ schema,
2581
+ parser
2582
+ };
2583
+ };
2584
+ function isValibotWrapper(schema) {
2585
+ return schema?._valibot === true;
2586
+ }
2587
+ async function validateValibotWrapper(wrapper, data) {
2588
+ const result = await wrapper.parser(wrapper.schema, data);
2589
+ if (!result.success) {
2590
+ throw new ValidationError(result.issues);
2591
+ }
2592
+ return result.output;
2593
+ }
2594
+ function isClass(schema) {
2595
+ try {
2596
+ if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
2597
+ return true;
2598
+ }
2599
+ return typeof schema === "function" && schema.prototype && schema.name;
2600
+ } catch {
2601
+ return false;
2602
+ }
2603
+ }
2604
+ async function validateClassValidator(schema, data) {
2605
+ const object = classTransformer.plainToInstance(schema, data);
2606
+ try {
2607
+ await classValidator.validateOrReject(object);
2608
+ return object;
2609
+ } catch (errors) {
2610
+ const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
2611
+ property: err.property,
2612
+ constraints: err.constraints,
2613
+ children: err.children
2614
+ })) : errors;
2615
+ throw new ValidationError(formattedErrors);
2616
+ }
2617
+ }
2618
+ const safelyGetBody = async (ctx) => {
2619
+ const req = ctx.req;
2620
+ if (req._bodyParsed) {
2621
+ return req._bodyValue;
2622
+ }
2623
+ try {
2624
+ let data;
2625
+ if (typeof req.json === "function") {
2626
+ data = await req.json();
2627
+ } else {
2628
+ data = req.body;
2629
+ if (typeof data === "string") {
2630
+ try {
2631
+ data = JSON.parse(data);
2632
+ } catch {
2633
+ }
2634
+ }
2635
+ }
2636
+ req._bodyParsed = true;
2637
+ req._bodyValue = data;
2638
+ Object.defineProperty(req, "json", {
2639
+ value: async () => req._bodyValue,
2640
+ configurable: true
2641
+ });
2642
+ return data;
2643
+ } catch (e) {
2644
+ return {};
2645
+ }
2646
+ };
2647
+ function validate(config) {
2648
+ return async (ctx, next) => {
2649
+ const dataToValidate = {};
2650
+ if (config.params) dataToValidate.params = ctx.params;
2651
+ let queryObj;
2652
+ if (config.query) {
2653
+ const url = new URL(ctx.req.url);
2654
+ queryObj = Object.fromEntries(url.searchParams.entries());
2655
+ dataToValidate.query = queryObj;
2656
+ }
2657
+ if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
2658
+ let body;
2659
+ if (config.body) {
2660
+ body = await safelyGetBody(ctx);
2661
+ dataToValidate.body = body;
2662
+ }
2663
+ if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2664
+ await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2665
+ }
2666
+ if (config.params) {
2667
+ ctx.params = await runValidation(config.params, ctx.params);
2668
+ }
2669
+ let validQuery;
2670
+ if (config.query && queryObj) {
2671
+ validQuery = await runValidation(config.query, queryObj);
2672
+ }
2673
+ if (config.headers) {
2674
+ const headersObj = Object.fromEntries(ctx.req.headers.entries());
2675
+ await runValidation(config.headers, headersObj);
2676
+ }
2677
+ let validBody;
2678
+ if (config.body) {
2679
+ const b = body ?? await safelyGetBody(ctx);
2680
+ validBody = await runValidation(config.body, b);
2681
+ const req = ctx.req;
2682
+ req._bodyValue = validBody;
2683
+ Object.defineProperty(req, "json", {
2684
+ value: async () => validBody,
2685
+ configurable: true
2686
+ });
2687
+ ctx.body = validBody;
2688
+ }
2689
+ if (ctx.app?.applicationConfig.hooks?.afterValidate) {
2690
+ const validatedData = { ...dataToValidate };
2691
+ if (config.params) validatedData.params = ctx.params;
2692
+ if (config.query) validatedData.query = validQuery;
2693
+ if (config.body) validatedData.body = validBody;
2694
+ await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
2695
+ }
2696
+ return next();
2697
+ };
2698
+ }
2699
+ async function runValidation(schema, data) {
2700
+ if (isZod(schema)) {
2701
+ return validateZod(schema, data);
2702
+ }
2703
+ if (isTypeBox(schema)) {
2704
+ return validateTypeBox(schema, data);
2705
+ }
2706
+ if (isAjv(schema)) {
2707
+ return validateAjv(schema, data);
2708
+ }
2709
+ if (isValibotWrapper(schema)) {
2710
+ return validateValibotWrapper(schema, data);
2711
+ }
2712
+ if (isClass(schema)) {
2713
+ return validateClassValidator(schema, data);
2714
+ }
2715
+ if (isTypeBox(schema)) {
2716
+ return validateTypeBox(schema, data);
2717
+ }
2718
+ if (isAjv(schema)) {
2719
+ return validateAjv(schema, data);
2720
+ }
2721
+ if (isValibotWrapper(schema)) {
2722
+ return validateValibotWrapper(schema, data);
2723
+ }
2724
+ if (typeof schema === "function") {
2725
+ return schema(data);
2726
+ }
2727
+ throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
2728
+ }
2729
+ const ajv = new Ajv({ coerceTypes: true, allErrors: true });
2730
+ addFormats(ajv);
2731
+ const compiledValidators = /* @__PURE__ */ new WeakMap();
2732
+ function openApiValidator() {
2733
+ return async (ctx, next) => {
2734
+ const app = ctx.app;
2735
+ if (!app || !app.openApiSpec) {
2736
+ return next();
2737
+ }
2738
+ let cache = compiledValidators.get(app);
2739
+ if (!cache) {
2740
+ cache = compileValidators(app.openApiSpec);
2741
+ compiledValidators.set(app, cache);
2742
+ }
2743
+ const method = ctx.req.method.toLowerCase();
2744
+ let matchPath;
2745
+ if (cache.has(ctx.path)) {
2746
+ matchPath = ctx.path;
2747
+ } else {
2748
+ for (const specPath of cache.keys()) {
2749
+ const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2750
+ const regex = new RegExp(regexStr);
2751
+ const match = regex.exec(ctx.path);
2752
+ if (match) {
2753
+ matchPath = specPath;
2754
+ break;
2755
+ }
2756
+ }
2757
+ }
2758
+ if (!matchPath) {
2759
+ return next();
2760
+ }
2761
+ const validators = cache.get(matchPath)?.[method];
2762
+ if (!validators) {
2763
+ return next();
2764
+ }
2765
+ const errors = [];
2766
+ if (validators.body) {
2767
+ let body;
2768
+ try {
2769
+ body = await ctx.req.json().catch(() => ({}));
2770
+ } catch {
2771
+ body = {};
2772
+ }
2773
+ const valid = validators.body(body);
2774
+ if (!valid && validators.body.errors) {
2775
+ errors.push(...validators.body.errors.map((e) => ({ ...e, location: "body" })));
2776
+ }
2777
+ }
2778
+ if (validators.query) {
2779
+ const query = Object.fromEntries(new URL(ctx.req.url).searchParams.entries());
2780
+ const valid = validators.query(query);
2781
+ if (!valid && validators.query.errors) {
2782
+ errors.push(...validators.query.errors.map((e) => ({ ...e, location: "query" })));
2783
+ }
2784
+ }
2785
+ if (validators.params) {
2786
+ let params = ctx.params;
2787
+ if (Object.keys(params).length === 0 && matchPath) {
2788
+ const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
2789
+ if (paramNames.length > 0) {
2790
+ const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2791
+ const regex = new RegExp(regexStr);
2792
+ const match = regex.exec(ctx.path);
2793
+ if (match) {
2794
+ params = {};
2795
+ paramNames.forEach((name, i) => {
2796
+ params[name] = match[i + 1];
2797
+ });
2798
+ }
2799
+ }
2800
+ }
2801
+ const valid = validators.params(params);
2802
+ if (!valid && validators.params.errors) {
2803
+ errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
2804
+ }
2805
+ }
2806
+ if (validators.headers) {
2807
+ const headers = Object.fromEntries(ctx.req.headers.entries());
2808
+ const valid = validators.headers(headers);
2809
+ if (!valid && validators.headers.errors) {
2810
+ errors.push(...validators.headers.errors.map((e) => ({ ...e, location: "header" })));
2811
+ }
2812
+ }
2813
+ if (errors.length > 0) {
2814
+ throw new ValidationError(errors);
2815
+ }
2816
+ return next();
2817
+ };
2818
+ }
2819
+ function compileValidators(spec) {
2820
+ const cache = /* @__PURE__ */ new Map();
2821
+ for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
2822
+ const pathValidators = {};
2823
+ for (const [method, operation] of Object.entries(pathItem)) {
2824
+ if (method === "parameters" || method === "summary" || method === "description") continue;
2825
+ const oper = operation;
2826
+ const validators = {};
2827
+ if (oper.requestBody?.content?.["application/json"]?.schema) {
2828
+ validators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
2829
+ }
2830
+ const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
2831
+ const queryProps = {};
2832
+ const pathProps = {};
2833
+ const headerProps = {};
2834
+ const queryRequired = [];
2835
+ const pathRequired = [];
2836
+ const headerRequired = [];
2837
+ for (const param of parameters) {
2838
+ if (param.in === "query") {
2839
+ queryProps[param.name] = param.schema || {};
2840
+ if (param.required) queryRequired.push(param.name);
2841
+ } else if (param.in === "path") {
2842
+ pathProps[param.name] = param.schema || {};
2843
+ pathRequired.push(param.name);
2844
+ } else if (param.in === "header") {
2845
+ headerProps[param.name] = param.schema || {};
2846
+ if (param.required) headerRequired.push(param.name);
2847
+ }
2848
+ }
2849
+ if (Object.keys(queryProps).length > 0) {
2850
+ validators.query = ajv.compile({
2851
+ type: "object",
2852
+ properties: queryProps,
2853
+ required: queryRequired.length > 0 ? queryRequired : void 0
2854
+ });
2855
+ }
2856
+ if (Object.keys(pathProps).length > 0) {
2857
+ validators.params = ajv.compile({
2858
+ type: "object",
2859
+ properties: pathProps,
2860
+ required: pathRequired.length > 0 ? pathRequired : void 0
2861
+ });
2862
+ }
2863
+ if (Object.keys(headerProps).length > 0) {
2864
+ validators.headers = ajv.compile({
2865
+ type: "object",
2866
+ properties: headerProps,
2867
+ required: headerRequired.length > 0 ? headerRequired : void 0
2868
+ });
2869
+ }
2870
+ pathValidators[method] = validators;
2871
+ }
2872
+ cache.set(path2, pathValidators);
2873
+ }
2874
+ return cache;
2875
+ }
2876
+ function RateLimit(options = {}) {
2877
+ const windowMs = options.windowMs || 60 * 1e3;
2878
+ const max = options.max || 5;
2879
+ const message = options.message || "Too many requests, please try again later.";
2880
+ const statusCode = options.statusCode || 429;
2881
+ const headers = options.headers !== false;
2882
+ const keyGenerator = options.keyGenerator || ((ctx) => {
2883
+ return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
2884
+ });
2885
+ const skip = options.skip || (() => false);
2886
+ const hits = /* @__PURE__ */ new Map();
2887
+ const interval = setInterval(() => {
2888
+ const now = Date.now();
2889
+ for (const [key, record] of hits.entries()) {
2890
+ if (record.resetTime <= now) {
2891
+ hits.delete(key);
2892
+ }
2893
+ }
2894
+ }, windowMs);
2895
+ interval.unref?.();
2896
+ return async (ctx, next) => {
2897
+ if (skip(ctx)) return next();
2898
+ const key = keyGenerator(ctx);
2899
+ const now = Date.now();
2900
+ let record = hits.get(key);
2901
+ if (!record || record.resetTime <= now) {
2902
+ record = {
2903
+ hits: 0,
2904
+ resetTime: now + windowMs
2905
+ };
2906
+ hits.set(key, record);
2907
+ }
2908
+ record.hits++;
2909
+ const remaining = Math.max(0, max - record.hits);
2910
+ const resetTime = Math.ceil(record.resetTime / 1e3);
2911
+ if (record.hits > max) {
2912
+ if (headers) {
2913
+ const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2914
+ res.headers.set("X-RateLimit-Limit", String(max));
2915
+ res.headers.set("X-RateLimit-Remaining", "0");
2916
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
2917
+ return res;
2918
+ }
2919
+ return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2920
+ }
2921
+ const response = await next();
2922
+ if (response instanceof Response && headers) {
2923
+ response.headers.set("X-RateLimit-Limit", String(max));
2924
+ response.headers.set("X-RateLimit-Remaining", String(remaining));
2925
+ response.headers.set("X-RateLimit-Reset", String(resetTime));
2926
+ }
2927
+ return response;
2928
+ };
2929
+ }
2930
+ const eta = new eta$2.Eta();
2931
+ class ScalarPlugin extends ShokupanRouter {
2932
+ constructor(pluginOptions) {
2933
+ super();
2934
+ this.pluginOptions = pluginOptions;
2935
+ this.init();
2936
+ }
2937
+ init() {
2938
+ this.get("/", (ctx) => {
2939
+ let path2 = ctx.url.toString();
2940
+ if (!path2.endsWith("/")) path2 += "/";
2941
+ return ctx.html(eta.renderString(`<!doctype html>
2388
2942
  <html>
2389
2943
  <head>
2390
2944
  <title>API Reference</title>
@@ -2752,194 +3306,6 @@ function Session(options) {
2752
3306
  return result;
2753
3307
  };
2754
3308
  }
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.");
2942
- }
2943
3309
  exports.$appRoot = $appRoot;
2944
3310
  exports.$childControllers = $childControllers;
2945
3311
  exports.$childRouters = $childRouters;
@@ -2994,6 +3360,7 @@ exports.Spec = Spec;
2994
3360
  exports.Use = Use;
2995
3361
  exports.ValidationError = ValidationError;
2996
3362
  exports.compose = compose;
3363
+ exports.openApiValidator = openApiValidator;
2997
3364
  exports.useExpress = useExpress;
2998
3365
  exports.valibot = valibot;
2999
3366
  exports.validate = validate;