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.js CHANGED
@@ -1,22 +1,25 @@
1
- import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
2
1
  import { Eta } from "eta";
3
2
  import { stat, readdir } from "fs/promises";
4
3
  import { resolve, join, basename } from "path";
5
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
6
6
  import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
7
7
  import * as jose from "jose";
8
- import { OpenAPIAnalyzer } from "./openapi-analyzer-cjdGeQ5a.js";
9
- import { randomUUID, createHmac } from "crypto";
10
- import { EventEmitter } from "events";
8
+ import Ajv from "ajv";
9
+ import addFormats from "ajv-formats";
11
10
  import { plainToInstance } from "class-transformer";
12
11
  import { validateOrReject } from "class-validator";
12
+ import { OpenAPIAnalyzer } from "./openapi-analyzer-BTExMLX4.js";
13
+ import { randomUUID, createHmac } from "crypto";
14
+ import { EventEmitter } from "events";
13
15
  class ShokupanResponse {
14
- _headers = new Headers();
16
+ _headers = null;
15
17
  _status = 200;
16
18
  /**
17
19
  * Get the current headers
18
20
  */
19
21
  get headers() {
22
+ if (!this._headers) this._headers = new Headers();
20
23
  return this._headers;
21
24
  }
22
25
  /**
@@ -37,6 +40,7 @@ class ShokupanResponse {
37
40
  * @param value Header value
38
41
  */
39
42
  set(key, value) {
43
+ if (!this._headers) this._headers = new Headers();
40
44
  this._headers.set(key, value);
41
45
  return this;
42
46
  }
@@ -46,6 +50,7 @@ class ShokupanResponse {
46
50
  * @param value Header value
47
51
  */
48
52
  append(key, value) {
53
+ if (!this._headers) this._headers = new Headers();
49
54
  this._headers.append(key, value);
50
55
  return this;
51
56
  }
@@ -54,29 +59,58 @@ class ShokupanResponse {
54
59
  * @param key Header name
55
60
  */
56
61
  get(key) {
57
- return this._headers.get(key);
62
+ return this._headers?.get(key) || null;
58
63
  }
59
64
  /**
60
65
  * Check if a header exists
61
66
  * @param key Header name
62
67
  */
63
68
  has(key) {
64
- return this._headers.has(key);
69
+ return this._headers?.has(key) || false;
70
+ }
71
+ /**
72
+ * Internal: check if headers have been initialized/modified
73
+ */
74
+ get hasPopulatedHeaders() {
75
+ return this._headers !== null;
65
76
  }
66
77
  }
67
78
  class ShokupanContext {
68
- constructor(request, server, state, app) {
79
+ constructor(request, server, state, app, enableMiddlewareTracking = false) {
69
80
  this.request = request;
70
81
  this.server = server;
71
82
  this.app = app;
72
- this.url = new URL(request.url);
73
83
  this.state = state || {};
84
+ if (enableMiddlewareTracking) {
85
+ const self = this;
86
+ this.state = new Proxy(this.state, {
87
+ set(target, p, newValue, receiver) {
88
+ const result = Reflect.set(target, p, newValue, receiver);
89
+ const currentHandler = self.handlerStack[self.handlerStack.length - 1];
90
+ if (currentHandler) {
91
+ if (!currentHandler.stateChanges) currentHandler.stateChanges = {};
92
+ currentHandler.stateChanges[p] = newValue;
93
+ }
94
+ return result;
95
+ }
96
+ });
97
+ }
74
98
  this.response = new ShokupanResponse();
75
99
  }
76
- url;
100
+ _url;
77
101
  params = {};
102
+ // Router assigns this, but default to empty object
78
103
  state;
104
+ handlerStack = [];
79
105
  response;
106
+ _finalResponse;
107
+ get url() {
108
+ if (!this._url) {
109
+ const urlString = this.request.url || "http://localhost/";
110
+ this._url = new URL(urlString);
111
+ }
112
+ return this._url;
113
+ }
80
114
  /**
81
115
  * Base request
82
116
  */
@@ -93,7 +127,26 @@ class ShokupanContext {
93
127
  * Request path
94
128
  */
95
129
  get path() {
96
- return this.url.pathname;
130
+ if (this._url) return this._url.pathname;
131
+ const url = this.request.url;
132
+ let queryIndex = url.indexOf("?");
133
+ const end = queryIndex === -1 ? url.length : queryIndex;
134
+ let start = 0;
135
+ const protocolIndex = url.indexOf("://");
136
+ if (protocolIndex !== -1) {
137
+ const hostStart = protocolIndex + 3;
138
+ const pathStart = url.indexOf("/", hostStart);
139
+ if (pathStart !== -1 && pathStart < end) {
140
+ start = pathStart;
141
+ } else {
142
+ return "/";
143
+ }
144
+ } else {
145
+ if (url.charCodeAt(0) === 47) {
146
+ start = 0;
147
+ }
148
+ }
149
+ return url.substring(start, end);
97
150
  }
98
151
  /**
99
152
  * Request query params
@@ -190,9 +243,25 @@ class ShokupanContext {
190
243
  return this;
191
244
  }
192
245
  mergeHeaders(headers) {
193
- const h = new Headers(this.response.headers);
246
+ let h;
247
+ if (this.response.hasPopulatedHeaders) {
248
+ h = new Headers(this.response.headers);
249
+ } else {
250
+ h = new Headers();
251
+ }
194
252
  if (headers) {
195
- new Headers(headers).forEach((v, k) => h.set(k, v));
253
+ if (headers instanceof Headers) {
254
+ headers.forEach((v, k) => h.set(k, v));
255
+ } else if (Array.isArray(headers)) {
256
+ headers.forEach(([k, v]) => h.set(k, v));
257
+ } else {
258
+ const keys = Object.keys(headers);
259
+ for (let i = 0; i < keys.length; i++) {
260
+ const key = keys[i];
261
+ const val = headers[key];
262
+ h.set(key, val);
263
+ }
264
+ }
196
265
  }
197
266
  return h;
198
267
  }
@@ -205,7 +274,8 @@ class ShokupanContext {
205
274
  send(body, options) {
206
275
  const headers = this.mergeHeaders(options?.headers);
207
276
  const status = options?.status ?? this.response.status;
208
- return new Response(body, { status, headers });
277
+ this._finalResponse = new Response(body, { status, headers });
278
+ return this._finalResponse;
209
279
  }
210
280
  /**
211
281
  * Read request body
@@ -224,19 +294,36 @@ class ShokupanContext {
224
294
  * Respond with a JSON object
225
295
  */
226
296
  json(data, status, headers) {
297
+ const finalStatus = status ?? this.response.status;
298
+ const jsonString = JSON.stringify(data);
299
+ if (!headers && !this.response.hasPopulatedHeaders) {
300
+ this._finalResponse = new Response(jsonString, {
301
+ status: finalStatus,
302
+ headers: { "content-type": "application/json" }
303
+ });
304
+ return this._finalResponse;
305
+ }
227
306
  const finalHeaders = this.mergeHeaders(headers);
228
307
  finalHeaders.set("content-type", "application/json");
229
- const finalStatus = status ?? this.response.status;
230
- return new Response(JSON.stringify(data), { status: finalStatus, headers: finalHeaders });
308
+ this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
309
+ return this._finalResponse;
231
310
  }
232
311
  /**
233
312
  * Respond with a text string
234
313
  */
235
314
  text(data, status, headers) {
315
+ const finalStatus = status ?? this.response.status;
316
+ if (!headers && !this.response.hasPopulatedHeaders) {
317
+ this._finalResponse = new Response(data, {
318
+ status: finalStatus,
319
+ headers: { "content-type": "text/plain" }
320
+ });
321
+ return this._finalResponse;
322
+ }
236
323
  const finalHeaders = this.mergeHeaders(headers);
237
324
  finalHeaders.set("content-type", "text/plain");
238
- const finalStatus = status ?? this.response.status;
239
- return new Response(data, { status: finalStatus, headers: finalHeaders });
325
+ this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
326
+ return this._finalResponse;
240
327
  }
241
328
  /**
242
329
  * Respond with HTML content
@@ -245,7 +332,8 @@ class ShokupanContext {
245
332
  const finalHeaders = this.mergeHeaders(headers);
246
333
  finalHeaders.set("content-type", "text/html");
247
334
  const finalStatus = status ?? this.response.status;
248
- return new Response(html, { status: finalStatus, headers: finalHeaders });
335
+ this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
336
+ return this._finalResponse;
249
337
  }
250
338
  /**
251
339
  * Respond with a redirect
@@ -253,7 +341,8 @@ class ShokupanContext {
253
341
  redirect(url, status = 302) {
254
342
  const headers = this.mergeHeaders();
255
343
  headers.set("Location", url);
256
- return new Response(null, { status, headers });
344
+ this._finalResponse = new Response(null, { status, headers });
345
+ return this._finalResponse;
257
346
  }
258
347
  /**
259
348
  * Respond with a status code
@@ -261,7 +350,8 @@ class ShokupanContext {
261
350
  */
262
351
  status(status) {
263
352
  const headers = this.mergeHeaders();
264
- return new Response(null, { status, headers });
353
+ this._finalResponse = new Response(null, { status, headers });
354
+ return this._finalResponse;
265
355
  }
266
356
  /**
267
357
  * Respond with a file
@@ -269,7 +359,8 @@ class ShokupanContext {
269
359
  file(path, fileOptions, responseOptions) {
270
360
  const headers = this.mergeHeaders(responseOptions?.headers);
271
361
  const status = responseOptions?.status ?? this.response.status;
272
- return new Response(Bun.file(path, fileOptions), { status, headers });
362
+ this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
363
+ return this._finalResponse;
273
364
  }
274
365
  /**
275
366
  * JSX Rendering Function
@@ -418,69 +509,29 @@ function Inject(token) {
418
509
  });
419
510
  };
420
511
  }
421
- const tracer = trace.getTracer("shokupan.middleware");
422
- function traceMiddleware(fn, name) {
423
- const middlewareName = fn.name || "anonymous middleware";
424
- return async (ctx, next) => {
425
- return tracer.startActiveSpan(`middleware - ${middlewareName}`, {
426
- kind: SpanKind.INTERNAL,
427
- attributes: {
428
- "code.function": middlewareName,
429
- "component": "shokupan.middleware"
430
- }
431
- }, async (span) => {
432
- try {
433
- const result = await fn(ctx, next);
434
- return result;
435
- } catch (err) {
436
- span.recordException(err);
437
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
438
- throw err;
439
- } finally {
440
- span.end();
441
- }
442
- });
443
- };
444
- }
445
- function traceHandler(fn, name) {
446
- return async function(...args) {
447
- return tracer.startActiveSpan(`route handler - ${name}`, {
448
- kind: SpanKind.INTERNAL,
449
- attributes: {
450
- "http.route": name,
451
- "component": "shokupan.route"
512
+ const compose = (middleware) => {
513
+ if (!middleware.length) {
514
+ return (context2, next) => {
515
+ return next ? next() : Promise.resolve();
516
+ };
517
+ }
518
+ return function dispatch(context2, next) {
519
+ let index = -1;
520
+ function runner(i) {
521
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
522
+ index = i;
523
+ if (i >= middleware.length) {
524
+ return next ? next() : Promise.resolve();
452
525
  }
453
- }, async (span) => {
526
+ const fn = middleware[i];
454
527
  try {
455
- const result = await fn.apply(this, args);
456
- return result;
528
+ return Promise.resolve(fn(context2, () => runner(i + 1)));
457
529
  } catch (err) {
458
- span.recordException(err);
459
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
460
- throw err;
461
- } finally {
462
- span.end();
530
+ return Promise.reject(err);
463
531
  }
464
- });
465
- };
466
- }
467
- const compose = (middleware) => {
468
- function fn(context2, next) {
469
- let runner = next || (async () => {
470
- });
471
- for (let i = middleware.length - 1; i >= 0; i--) {
472
- const fn2 = traceMiddleware(middleware[i]);
473
- const nextStep = runner;
474
- let called = false;
475
- runner = async () => {
476
- if (called) throw new Error("next() called multiple times");
477
- called = true;
478
- return fn2(context2, nextStep);
479
- };
480
532
  }
481
- return runner();
482
- }
483
- return fn;
533
+ return runner(0);
534
+ };
484
535
  };
485
536
  class ShokupanRequestBase {
486
537
  method;
@@ -707,7 +758,7 @@ async function generateOpenApi(rootRouter, options = {}) {
707
758
  const defaultTagName = options.defaultTag || "Application";
708
759
  let astRoutes = [];
709
760
  try {
710
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-cjdGeQ5a.js");
761
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-BTExMLX4.js");
711
762
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
712
763
  const { applications } = await analyzer.analyze();
713
764
  const appMap = /* @__PURE__ */ new Map();
@@ -1107,6 +1158,29 @@ function serveStatic(ctx, config, prefix) {
1107
1158
  };
1108
1159
  }
1109
1160
  const asyncContext = new AsyncLocalStorage();
1161
+ const tracer = trace.getTracer("shokupan.middleware");
1162
+ function traceHandler(fn, name) {
1163
+ return async function(...args) {
1164
+ return tracer.startActiveSpan(`route handler - ${name}`, {
1165
+ kind: SpanKind.INTERNAL,
1166
+ attributes: {
1167
+ "http.route": name,
1168
+ "component": "shokupan.route"
1169
+ }
1170
+ }, async (span) => {
1171
+ try {
1172
+ const result = await fn.apply(this, args);
1173
+ return result;
1174
+ } catch (err) {
1175
+ span.recordException(err);
1176
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
1177
+ throw err;
1178
+ } finally {
1179
+ span.end();
1180
+ }
1181
+ });
1182
+ };
1183
+ }
1110
1184
  const RouterRegistry = /* @__PURE__ */ new Map();
1111
1185
  const ShokupanApplicationTree = {};
1112
1186
  class ShokupanRouter {
@@ -1297,7 +1371,7 @@ class ShokupanRouter {
1297
1371
  }
1298
1372
  }
1299
1373
  }
1300
- const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
1374
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1301
1375
  return tracedOriginalHandler.apply(instance, args);
1302
1376
  };
1303
1377
  let finalHandler = wrappedHandler;
@@ -1433,6 +1507,7 @@ class ShokupanRouter {
1433
1507
  applyHooks(match) {
1434
1508
  if (!this.config?.hooks) return match;
1435
1509
  const hooks = this.config.hooks;
1510
+ if (!hooks.onRequestStart && !hooks.onRequestEnd && !hooks.onError) return match;
1436
1511
  const originalHandler = match.handler;
1437
1512
  match.handler = async (ctx) => {
1438
1513
  if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
@@ -1461,16 +1536,25 @@ class ShokupanRouter {
1461
1536
  * @returns Route handler and parameters if found, otherwise null
1462
1537
  */
1463
1538
  find(method, path) {
1464
- for (const route of this[$routes]) {
1465
- if (route.method !== "ALL" && route.method !== method) continue;
1466
- const match = route.regex.exec(path);
1467
- if (match) {
1468
- const params = {};
1469
- route.keys.forEach((key, index) => {
1470
- params[key] = match[index + 1];
1471
- });
1472
- return this.applyHooks({ handler: route.handler, params });
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
+ }
1473
1550
  }
1551
+ return null;
1552
+ };
1553
+ let result = findInRoutes(this[$routes], method);
1554
+ if (result) return result;
1555
+ if (method === "HEAD") {
1556
+ result = findInRoutes(this[$routes], "GET");
1557
+ if (result) return result;
1474
1558
  }
1475
1559
  for (const child of this[$childRouters]) {
1476
1560
  const prefix = child[$mountPath];
@@ -1563,6 +1647,35 @@ class ShokupanRouter {
1563
1647
  return innerHandler(ctx);
1564
1648
  };
1565
1649
  }
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;
1668
+ wrappedHandler = async (ctx) => {
1669
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1670
+ ctx.handlerStack.push({
1671
+ name: handler.name || "anonymous",
1672
+ file,
1673
+ line
1674
+ });
1675
+ }
1676
+ return trackedHandler(ctx);
1677
+ };
1678
+ wrappedHandler.originalHandler = trackedHandler.originalHandler || trackedHandler;
1566
1679
  this[$routes].push({
1567
1680
  method,
1568
1681
  path,
@@ -1608,7 +1721,35 @@ class ShokupanRouter {
1608
1721
  guard(specOrHandler, handler) {
1609
1722
  const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
1610
1723
  const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
1611
- this.currentGuards.push({ handler: guardHandler, spec });
1724
+ let file = "unknown";
1725
+ let line = 0;
1726
+ try {
1727
+ const err = new Error();
1728
+ const stack = err.stack?.split("\n") || [];
1729
+ const callerLine = stack.find(
1730
+ (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
1731
+ );
1732
+ if (callerLine) {
1733
+ const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1734
+ if (match) {
1735
+ file = match[1];
1736
+ line = parseInt(match[2], 10);
1737
+ }
1738
+ }
1739
+ } catch (e) {
1740
+ }
1741
+ const trackedGuard = async (ctx, next) => {
1742
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1743
+ ctx.handlerStack.push({
1744
+ name: guardHandler.name || "guard",
1745
+ file,
1746
+ line
1747
+ });
1748
+ }
1749
+ return guardHandler(ctx, next);
1750
+ };
1751
+ trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
1752
+ this.currentGuards.push({ handler: trackedGuard, spec });
1612
1753
  return this;
1613
1754
  }
1614
1755
  /**
@@ -1696,6 +1837,7 @@ class Shokupan extends ShokupanRouter {
1696
1837
  applicationConfig = {};
1697
1838
  openApiSpec;
1698
1839
  middleware = [];
1840
+ composedMiddleware;
1699
1841
  get logger() {
1700
1842
  return this.applicationConfig.logger;
1701
1843
  }
@@ -1709,7 +1851,37 @@ class Shokupan extends ShokupanRouter {
1709
1851
  * Adds middleware to the application.
1710
1852
  */
1711
1853
  use(middleware) {
1712
- this.middleware.push(middleware);
1854
+ 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) {
1872
+ }
1873
+ trackedMiddleware = async (ctx, next) => {
1874
+ const c = ctx;
1875
+ if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
1876
+ c.handlerStack.push({
1877
+ name: middleware.name || "middleware",
1878
+ file,
1879
+ line
1880
+ });
1881
+ }
1882
+ return middleware(ctx, next);
1883
+ };
1884
+ this.middleware.push(trackedMiddleware);
1713
1885
  return this;
1714
1886
  }
1715
1887
  startupHooks = [];
@@ -1744,9 +1916,28 @@ class Shokupan extends ShokupanRouter {
1744
1916
  development: this.applicationConfig.development,
1745
1917
  fetch: this.fetch.bind(this),
1746
1918
  reusePort: this.applicationConfig.reusePort,
1747
- idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0
1919
+ idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
1920
+ websocket: {
1921
+ open(ws) {
1922
+ ws.data?.handler?.open?.(ws);
1923
+ },
1924
+ message(ws, message) {
1925
+ ws.data?.handler?.message?.(ws, message);
1926
+ },
1927
+ drain(ws) {
1928
+ ws.data?.handler?.drain?.(ws);
1929
+ },
1930
+ close(ws, code, reason) {
1931
+ ws.data?.handler?.close?.(ws, code, reason);
1932
+ }
1933
+ }
1748
1934
  };
1749
- const server = this.applicationConfig.serverFactory ? await this.applicationConfig.serverFactory(serveOptions) : Bun.serve(serveOptions);
1935
+ let factory = this.applicationConfig.serverFactory;
1936
+ if (!factory && typeof Bun === "undefined") {
1937
+ const { createHttpServer } = await import("./server-adapter-CnQFr4P7.js");
1938
+ factory = createHttpServer();
1939
+ }
1940
+ const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
1750
1941
  console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
1751
1942
  return server;
1752
1943
  }
@@ -1801,108 +1992,118 @@ class Shokupan extends ShokupanRouter {
1801
1992
  * @returns The response to send.
1802
1993
  */
1803
1994
  async fetch(req, server) {
1804
- const tracer2 = trace.getTracer("shokupan.application");
1805
- const store = asyncContext.getStore();
1806
- const attrs = {
1807
- attributes: {
1808
- "http.url": req.url,
1809
- "http.method": req.method
1810
- }
1811
- };
1812
- const parent = store?.get("span");
1813
- const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
1814
- return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
1995
+ if (this.applicationConfig.enableTracing) {
1996
+ const tracer2 = trace.getTracer("shokupan.application");
1997
+ const store = asyncContext.getStore();
1998
+ const attrs = {
1999
+ attributes: {
2000
+ "http.url": req.url,
2001
+ "http.method": req.method
2002
+ }
2003
+ };
2004
+ const parent = store?.get("span");
2005
+ const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
2006
+ return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2007
+ const ctxMap = /* @__PURE__ */ new Map();
2008
+ ctxMap.set("span", span);
2009
+ ctxMap.set("request", req);
2010
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
2011
+ });
2012
+ }
2013
+ if (this.applicationConfig.enableAsyncLocalStorage) {
1815
2014
  const ctxMap = /* @__PURE__ */ new Map();
1816
- ctxMap.set("span", span);
1817
2015
  ctxMap.set("request", req);
1818
- const runCallback = () => {
1819
- const request = req;
1820
- const ctx2 = new ShokupanContext(request, server, void 0, this);
1821
- const handle = async () => {
1822
- try {
1823
- if (this.applicationConfig.hooks?.onRequestStart) {
1824
- await this.applicationConfig.hooks.onRequestStart(ctx2);
1825
- }
1826
- const fn = compose(this.middleware);
1827
- const result = await fn(ctx2, async () => {
1828
- const match = this.find(req.method, ctx2.path);
1829
- if (match) {
1830
- ctx2.params = match.params;
1831
- return match.handler(ctx2);
1832
- }
1833
- return null;
1834
- });
1835
- let response;
1836
- if (result instanceof Response) {
1837
- response = result;
1838
- } else if (result === null || result === void 0) {
1839
- span.setAttribute("http.status_code", 404);
1840
- response = ctx2.text("Not Found", 404);
1841
- } else if (typeof result === "object") {
1842
- response = ctx2.json(result);
1843
- } else {
1844
- response = ctx2.text(String(result));
1845
- }
1846
- if (this.applicationConfig.hooks?.onRequestEnd) {
1847
- await this.applicationConfig.hooks.onRequestEnd(ctx2);
1848
- }
1849
- if (this.applicationConfig.hooks?.onResponseStart) {
1850
- await this.applicationConfig.hooks.onResponseStart(ctx2, response);
1851
- }
1852
- return response;
1853
- } catch (err) {
1854
- console.error(err);
1855
- span.recordException(err);
1856
- span.setStatus({ code: 2 });
1857
- const status = err.status || err.statusCode || 500;
1858
- const body = { error: err.message || "Internal Server Error" };
1859
- if (err.errors) body.errors = err.errors;
1860
- if (this.applicationConfig.hooks?.onError) {
1861
- try {
1862
- await this.applicationConfig.hooks.onError(err, ctx2);
1863
- } catch (hookErr) {
1864
- console.error("Error in onError hook:", hookErr);
1865
- }
1866
- }
1867
- return ctx2.json(body, status);
2016
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
2017
+ }
2018
+ return this.handleRequest(req, server);
2019
+ }
2020
+ async handleRequest(req, server) {
2021
+ const request = req;
2022
+ const ctx = new ShokupanContext(request, server, void 0, this, this.applicationConfig.enableMiddlewareTracking);
2023
+ const handle = async () => {
2024
+ try {
2025
+ if (this.applicationConfig.hooks?.onRequestStart) {
2026
+ await this.applicationConfig.hooks.onRequestStart(ctx);
2027
+ }
2028
+ const fn = this.composedMiddleware ??= compose(this.middleware);
2029
+ const result = await fn(ctx, async () => {
2030
+ const match = this.find(req.method, ctx.path);
2031
+ if (match) {
2032
+ ctx.params = match.params;
2033
+ return match.handler(ctx);
1868
2034
  }
1869
- };
1870
- let executionPromise = handle();
1871
- const timeoutMs = this.applicationConfig.requestTimeout;
1872
- if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
1873
- let timeoutId;
1874
- const timeoutPromise = new Promise((_, reject) => {
1875
- timeoutId = setTimeout(async () => {
1876
- try {
1877
- if (this.applicationConfig.hooks?.onRequestTimeout) {
1878
- await this.applicationConfig.hooks.onRequestTimeout(ctx2);
1879
- }
1880
- } catch (e) {
1881
- console.error("Error in onRequestTimeout hook:", e);
1882
- }
1883
- reject(new Error("Request Timeout"));
1884
- }, timeoutMs);
1885
- });
1886
- executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2035
+ return null;
2036
+ });
2037
+ let response;
2038
+ if (result instanceof Response) {
2039
+ response = result;
2040
+ } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2041
+ 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
+ } else if (result === null || result === void 0) {
2047
+ if (ctx._finalResponse) response = ctx._finalResponse;
2048
+ else response = ctx.text("Not Found", 404);
2049
+ } else if (typeof result === "object") {
2050
+ response = ctx.json(result);
2051
+ } else {
2052
+ response = ctx.text(String(result));
2053
+ }
2054
+ if (this.applicationConfig.hooks?.onRequestEnd) {
2055
+ await this.applicationConfig.hooks.onRequestEnd(ctx);
1887
2056
  }
1888
- return executionPromise.catch((err) => {
1889
- if (err.message === "Request Timeout") {
1890
- return ctx2.text("Request Timeout", 408);
2057
+ if (this.applicationConfig.hooks?.onResponseStart) {
2058
+ await this.applicationConfig.hooks.onResponseStart(ctx, response);
2059
+ }
2060
+ return response;
2061
+ } catch (err) {
2062
+ console.error(err);
2063
+ const span = asyncContext.getStore()?.get("span");
2064
+ if (span) span.setStatus({ code: 2 });
2065
+ const status = err.status || err.statusCode || 500;
2066
+ const body = { error: err.message || "Internal Server Error" };
2067
+ 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);
1891
2073
  }
1892
- console.error("Unexpected error in request execution:", err);
1893
- return ctx2.text("Internal Server Error", 500);
1894
- }).then(async (res) => {
1895
- if (this.applicationConfig.hooks?.onResponseEnd) {
1896
- await this.applicationConfig.hooks.onResponseEnd(ctx2, res);
2074
+ }
2075
+ return ctx.json(body, status);
2076
+ }
2077
+ };
2078
+ let executionPromise = handle();
2079
+ const timeoutMs = this.applicationConfig.requestTimeout;
2080
+ if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
2081
+ let timeoutId;
2082
+ const timeoutPromise = new Promise((_, reject) => {
2083
+ 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);
1897
2090
  }
1898
- return res;
1899
- }).finally(() => span.end());
1900
- };
1901
- if (this.applicationConfig.enableAsyncLocalStorage) {
1902
- return asyncContext.run(ctxMap, runCallback);
1903
- } else {
1904
- return runCallback();
2091
+ reject(new Error("Request Timeout"));
2092
+ }, timeoutMs);
2093
+ });
2094
+ executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2095
+ }
2096
+ return executionPromise.catch((err) => {
2097
+ if (err.message === "Request Timeout") {
2098
+ return ctx.text("Request Timeout", 408);
2099
+ }
2100
+ console.error("Unexpected error in request execution:", err);
2101
+ return ctx.text("Internal Server Error", 500);
2102
+ }).then(async (res) => {
2103
+ if (this.applicationConfig.hooks?.onResponseEnd) {
2104
+ await this.applicationConfig.hooks.onResponseEnd(ctx, res);
1905
2105
  }
2106
+ return res;
1906
2107
  });
1907
2108
  }
1908
2109
  }
@@ -2129,15 +2330,19 @@ class AuthPlugin extends ShokupanRouter {
2129
2330
  }
2130
2331
  }
2131
2332
  function Compression(options = {}) {
2132
- const threshold = options.threshold ?? 1024;
2333
+ const threshold = options.threshold ?? 512;
2133
2334
  return async (ctx, next) => {
2134
2335
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
2135
2336
  let method = null;
2136
2337
  if (acceptEncoding.includes("br")) method = "br";
2338
+ else if (acceptEncoding.includes("zstd")) method = "zstd";
2137
2339
  else if (acceptEncoding.includes("gzip")) method = "gzip";
2138
2340
  else if (acceptEncoding.includes("deflate")) method = "deflate";
2139
2341
  if (!method) return next();
2140
- const response = await next();
2342
+ let response = await next();
2343
+ if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
2344
+ response = ctx._finalResponse;
2345
+ }
2141
2346
  if (response instanceof Response) {
2142
2347
  if (response.headers.has("Content-Encoding")) return response;
2143
2348
  const body = await response.arrayBuffer();
@@ -2149,17 +2354,31 @@ function Compression(options = {}) {
2149
2354
  });
2150
2355
  }
2151
2356
  let compressed;
2152
- if (method === "br") {
2153
- compressed = require("node:zlib").brotliCompressSync(body);
2154
- } else if (method === "gzip") {
2155
- compressed = Bun.gzipSync(body);
2156
- } else {
2157
- compressed = Bun.deflateSync(body);
2357
+ switch (method) {
2358
+ case "br":
2359
+ const zlib = require("node:zlib");
2360
+ compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
2361
+ params: {
2362
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 4
2363
+ }
2364
+ }, (err, data) => {
2365
+ if (err) return rej(err);
2366
+ res(data);
2367
+ }));
2368
+ break;
2369
+ case "gzip":
2370
+ compressed = Bun.gzipSync(body);
2371
+ break;
2372
+ case "zstd":
2373
+ compressed = await Bun.zstdCompress(body);
2374
+ break;
2375
+ default:
2376
+ compressed = Bun.deflateSync(body);
2377
+ break;
2158
2378
  }
2159
2379
  const headers = new Headers(response.headers);
2160
2380
  headers.set("Content-Encoding", method);
2161
2381
  headers.set("Content-Length", String(compressed.length));
2162
- headers.delete("Content-Length");
2163
2382
  return new Response(compressed, {
2164
2383
  status: response.status,
2165
2384
  statusText: response.statusText,
@@ -2300,72 +2519,407 @@ function useExpress(expressMiddleware) {
2300
2519
  });
2301
2520
  };
2302
2521
  }
2303
- function RateLimit(options = {}) {
2304
- const windowMs = options.windowMs || 60 * 1e3;
2305
- const max = options.max || 5;
2306
- const message = options.message || "Too many requests, please try again later.";
2307
- const statusCode = options.statusCode || 429;
2308
- const headers = options.headers !== false;
2309
- const keyGenerator = options.keyGenerator || ((ctx) => {
2310
- return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
2311
- });
2312
- const skip = options.skip || (() => false);
2313
- const hits = /* @__PURE__ */ new Map();
2314
- const interval = setInterval(() => {
2315
- const now = Date.now();
2316
- for (const [key, record] of hits.entries()) {
2317
- if (record.resetTime <= now) {
2318
- hits.delete(key);
2319
- }
2320
- }
2321
- }, windowMs);
2322
- if (interval.unref) interval.unref();
2323
- return async (ctx, next) => {
2324
- if (skip(ctx)) return next();
2325
- const key = keyGenerator(ctx);
2326
- const now = Date.now();
2327
- let record = hits.get(key);
2328
- if (!record || record.resetTime <= now) {
2329
- record = {
2330
- hits: 0,
2331
- resetTime: now + windowMs
2332
- };
2333
- hits.set(key, record);
2334
- }
2335
- record.hits++;
2336
- const remaining = Math.max(0, max - record.hits);
2337
- const resetTime = Math.ceil(record.resetTime / 1e3);
2338
- if (record.hits > max) {
2339
- if (headers) {
2340
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2341
- res.headers.set("X-RateLimit-Limit", String(max));
2342
- res.headers.set("X-RateLimit-Remaining", "0");
2343
- res.headers.set("X-RateLimit-Reset", String(resetTime));
2344
- return res;
2345
- }
2346
- return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2347
- }
2348
- const response = await next();
2349
- if (response instanceof Response && headers) {
2350
- response.headers.set("X-RateLimit-Limit", String(max));
2351
- response.headers.set("X-RateLimit-Remaining", String(remaining));
2352
- response.headers.set("X-RateLimit-Reset", String(resetTime));
2353
- }
2354
- return response;
2355
- };
2356
- }
2357
- const eta = new Eta();
2358
- class ScalarPlugin extends ShokupanRouter {
2359
- constructor(pluginOptions) {
2360
- super();
2361
- this.pluginOptions = pluginOptions;
2362
- this.init();
2522
+ class ValidationError extends Error {
2523
+ constructor(errors) {
2524
+ super("Validation Error");
2525
+ this.errors = errors;
2363
2526
  }
2364
- init() {
2365
- this.get("/", (ctx) => {
2366
- let path = ctx.url.toString();
2367
- if (!path.endsWith("/")) path += "/";
2368
- return ctx.html(eta.renderString(`<!doctype html>
2527
+ status = 400;
2528
+ }
2529
+ function isZod(schema) {
2530
+ return typeof schema?.safeParse === "function";
2531
+ }
2532
+ async function validateZod(schema, data) {
2533
+ const result = await schema.safeParseAsync(data);
2534
+ if (!result.success) {
2535
+ throw new ValidationError(result.error.errors);
2536
+ }
2537
+ return result.data;
2538
+ }
2539
+ function isTypeBox(schema) {
2540
+ return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
2541
+ }
2542
+ function validateTypeBox(schema, data) {
2543
+ if (!schema.Check(data)) {
2544
+ throw new ValidationError([...schema.Errors(data)]);
2545
+ }
2546
+ return data;
2547
+ }
2548
+ function isAjv(schema) {
2549
+ return typeof schema === "function" && "errors" in schema;
2550
+ }
2551
+ function validateAjv(schema, data) {
2552
+ const valid = schema(data);
2553
+ if (!valid) {
2554
+ throw new ValidationError(schema.errors);
2555
+ }
2556
+ return data;
2557
+ }
2558
+ const valibot = (schema, parser) => {
2559
+ return {
2560
+ _valibot: true,
2561
+ schema,
2562
+ parser
2563
+ };
2564
+ };
2565
+ function isValibotWrapper(schema) {
2566
+ return schema?._valibot === true;
2567
+ }
2568
+ async function validateValibotWrapper(wrapper, data) {
2569
+ const result = await wrapper.parser(wrapper.schema, data);
2570
+ if (!result.success) {
2571
+ throw new ValidationError(result.issues);
2572
+ }
2573
+ return result.output;
2574
+ }
2575
+ function isClass(schema) {
2576
+ try {
2577
+ if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
2578
+ return true;
2579
+ }
2580
+ return typeof schema === "function" && schema.prototype && schema.name;
2581
+ } catch {
2582
+ return false;
2583
+ }
2584
+ }
2585
+ async function validateClassValidator(schema, data) {
2586
+ const object = plainToInstance(schema, data);
2587
+ try {
2588
+ await validateOrReject(object);
2589
+ return object;
2590
+ } catch (errors) {
2591
+ const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
2592
+ property: err.property,
2593
+ constraints: err.constraints,
2594
+ children: err.children
2595
+ })) : errors;
2596
+ throw new ValidationError(formattedErrors);
2597
+ }
2598
+ }
2599
+ const safelyGetBody = async (ctx) => {
2600
+ const req = ctx.req;
2601
+ if (req._bodyParsed) {
2602
+ return req._bodyValue;
2603
+ }
2604
+ try {
2605
+ let data;
2606
+ if (typeof req.json === "function") {
2607
+ data = await req.json();
2608
+ } else {
2609
+ data = req.body;
2610
+ if (typeof data === "string") {
2611
+ try {
2612
+ data = JSON.parse(data);
2613
+ } catch {
2614
+ }
2615
+ }
2616
+ }
2617
+ req._bodyParsed = true;
2618
+ req._bodyValue = data;
2619
+ Object.defineProperty(req, "json", {
2620
+ value: async () => req._bodyValue,
2621
+ configurable: true
2622
+ });
2623
+ return data;
2624
+ } catch (e) {
2625
+ return {};
2626
+ }
2627
+ };
2628
+ function validate(config) {
2629
+ return async (ctx, next) => {
2630
+ const dataToValidate = {};
2631
+ if (config.params) dataToValidate.params = ctx.params;
2632
+ let queryObj;
2633
+ if (config.query) {
2634
+ const url = new URL(ctx.req.url);
2635
+ queryObj = Object.fromEntries(url.searchParams.entries());
2636
+ dataToValidate.query = queryObj;
2637
+ }
2638
+ if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
2639
+ let body;
2640
+ if (config.body) {
2641
+ body = await safelyGetBody(ctx);
2642
+ dataToValidate.body = body;
2643
+ }
2644
+ if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2645
+ await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2646
+ }
2647
+ if (config.params) {
2648
+ ctx.params = await runValidation(config.params, ctx.params);
2649
+ }
2650
+ let validQuery;
2651
+ if (config.query && queryObj) {
2652
+ validQuery = await runValidation(config.query, queryObj);
2653
+ }
2654
+ if (config.headers) {
2655
+ const headersObj = Object.fromEntries(ctx.req.headers.entries());
2656
+ await runValidation(config.headers, headersObj);
2657
+ }
2658
+ let validBody;
2659
+ if (config.body) {
2660
+ const b = body ?? await safelyGetBody(ctx);
2661
+ validBody = await runValidation(config.body, b);
2662
+ const req = ctx.req;
2663
+ req._bodyValue = validBody;
2664
+ Object.defineProperty(req, "json", {
2665
+ value: async () => validBody,
2666
+ configurable: true
2667
+ });
2668
+ ctx.body = validBody;
2669
+ }
2670
+ if (ctx.app?.applicationConfig.hooks?.afterValidate) {
2671
+ const validatedData = { ...dataToValidate };
2672
+ if (config.params) validatedData.params = ctx.params;
2673
+ if (config.query) validatedData.query = validQuery;
2674
+ if (config.body) validatedData.body = validBody;
2675
+ await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
2676
+ }
2677
+ return next();
2678
+ };
2679
+ }
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
+ const ajv = new Ajv({ coerceTypes: true, allErrors: true });
2711
+ addFormats(ajv);
2712
+ const compiledValidators = /* @__PURE__ */ new WeakMap();
2713
+ function openApiValidator() {
2714
+ return async (ctx, next) => {
2715
+ const app = ctx.app;
2716
+ if (!app || !app.openApiSpec) {
2717
+ return next();
2718
+ }
2719
+ let cache = compiledValidators.get(app);
2720
+ if (!cache) {
2721
+ cache = compileValidators(app.openApiSpec);
2722
+ compiledValidators.set(app, cache);
2723
+ }
2724
+ const method = ctx.req.method.toLowerCase();
2725
+ let matchPath;
2726
+ if (cache.has(ctx.path)) {
2727
+ matchPath = ctx.path;
2728
+ } else {
2729
+ for (const specPath of cache.keys()) {
2730
+ const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2731
+ const regex = new RegExp(regexStr);
2732
+ const match = regex.exec(ctx.path);
2733
+ if (match) {
2734
+ matchPath = specPath;
2735
+ break;
2736
+ }
2737
+ }
2738
+ }
2739
+ if (!matchPath) {
2740
+ return next();
2741
+ }
2742
+ const validators = cache.get(matchPath)?.[method];
2743
+ if (!validators) {
2744
+ return next();
2745
+ }
2746
+ const errors = [];
2747
+ if (validators.body) {
2748
+ let body;
2749
+ try {
2750
+ body = await ctx.req.json().catch(() => ({}));
2751
+ } catch {
2752
+ body = {};
2753
+ }
2754
+ const valid = validators.body(body);
2755
+ if (!valid && validators.body.errors) {
2756
+ errors.push(...validators.body.errors.map((e) => ({ ...e, location: "body" })));
2757
+ }
2758
+ }
2759
+ if (validators.query) {
2760
+ const query = Object.fromEntries(new URL(ctx.req.url).searchParams.entries());
2761
+ const valid = validators.query(query);
2762
+ if (!valid && validators.query.errors) {
2763
+ errors.push(...validators.query.errors.map((e) => ({ ...e, location: "query" })));
2764
+ }
2765
+ }
2766
+ 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
+ }
2782
+ const valid = validators.params(params);
2783
+ if (!valid && validators.params.errors) {
2784
+ errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
2785
+ }
2786
+ }
2787
+ if (validators.headers) {
2788
+ const headers = Object.fromEntries(ctx.req.headers.entries());
2789
+ const valid = validators.headers(headers);
2790
+ if (!valid && validators.headers.errors) {
2791
+ errors.push(...validators.headers.errors.map((e) => ({ ...e, location: "header" })));
2792
+ }
2793
+ }
2794
+ if (errors.length > 0) {
2795
+ throw new ValidationError(errors);
2796
+ }
2797
+ return next();
2798
+ };
2799
+ }
2800
+ function compileValidators(spec) {
2801
+ const cache = /* @__PURE__ */ new Map();
2802
+ for (const [path, pathItem] of Object.entries(spec.paths || {})) {
2803
+ const pathValidators = {};
2804
+ for (const [method, operation] of Object.entries(pathItem)) {
2805
+ if (method === "parameters" || method === "summary" || method === "description") continue;
2806
+ const oper = operation;
2807
+ const validators = {};
2808
+ if (oper.requestBody?.content?.["application/json"]?.schema) {
2809
+ validators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
2810
+ }
2811
+ const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
2812
+ const queryProps = {};
2813
+ const pathProps = {};
2814
+ const headerProps = {};
2815
+ const queryRequired = [];
2816
+ const pathRequired = [];
2817
+ const headerRequired = [];
2818
+ for (const param of parameters) {
2819
+ if (param.in === "query") {
2820
+ queryProps[param.name] = param.schema || {};
2821
+ if (param.required) queryRequired.push(param.name);
2822
+ } else if (param.in === "path") {
2823
+ pathProps[param.name] = param.schema || {};
2824
+ pathRequired.push(param.name);
2825
+ } else if (param.in === "header") {
2826
+ headerProps[param.name] = param.schema || {};
2827
+ if (param.required) headerRequired.push(param.name);
2828
+ }
2829
+ }
2830
+ if (Object.keys(queryProps).length > 0) {
2831
+ validators.query = ajv.compile({
2832
+ type: "object",
2833
+ properties: queryProps,
2834
+ required: queryRequired.length > 0 ? queryRequired : void 0
2835
+ });
2836
+ }
2837
+ if (Object.keys(pathProps).length > 0) {
2838
+ validators.params = ajv.compile({
2839
+ type: "object",
2840
+ properties: pathProps,
2841
+ required: pathRequired.length > 0 ? pathRequired : void 0
2842
+ });
2843
+ }
2844
+ if (Object.keys(headerProps).length > 0) {
2845
+ validators.headers = ajv.compile({
2846
+ type: "object",
2847
+ properties: headerProps,
2848
+ required: headerRequired.length > 0 ? headerRequired : void 0
2849
+ });
2850
+ }
2851
+ pathValidators[method] = validators;
2852
+ }
2853
+ cache.set(path, pathValidators);
2854
+ }
2855
+ return cache;
2856
+ }
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";
2865
+ });
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
+ }
2911
+ const eta = new Eta();
2912
+ class ScalarPlugin extends ShokupanRouter {
2913
+ constructor(pluginOptions) {
2914
+ super();
2915
+ this.pluginOptions = pluginOptions;
2916
+ this.init();
2917
+ }
2918
+ init() {
2919
+ this.get("/", (ctx) => {
2920
+ let path = ctx.url.toString();
2921
+ if (!path.endsWith("/")) path += "/";
2922
+ return ctx.html(eta.renderString(`<!doctype html>
2369
2923
  <html>
2370
2924
  <head>
2371
2925
  <title>API Reference</title>
@@ -2733,194 +3287,6 @@ function Session(options) {
2733
3287
  return result;
2734
3288
  };
2735
3289
  }
2736
- class ValidationError extends Error {
2737
- constructor(errors) {
2738
- super("Validation Error");
2739
- this.errors = errors;
2740
- }
2741
- status = 400;
2742
- }
2743
- function isZod(schema) {
2744
- return typeof schema?.safeParse === "function";
2745
- }
2746
- async function validateZod(schema, data) {
2747
- const result = await schema.safeParseAsync(data);
2748
- if (!result.success) {
2749
- throw new ValidationError(result.error.errors);
2750
- }
2751
- return result.data;
2752
- }
2753
- function isTypeBox(schema) {
2754
- return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
2755
- }
2756
- function validateTypeBox(schema, data) {
2757
- if (!schema.Check(data)) {
2758
- throw new ValidationError([...schema.Errors(data)]);
2759
- }
2760
- return data;
2761
- }
2762
- function isAjv(schema) {
2763
- return typeof schema === "function" && "errors" in schema;
2764
- }
2765
- function validateAjv(schema, data) {
2766
- const valid = schema(data);
2767
- if (!valid) {
2768
- throw new ValidationError(schema.errors);
2769
- }
2770
- return data;
2771
- }
2772
- const valibot = (schema, parser) => {
2773
- return {
2774
- _valibot: true,
2775
- schema,
2776
- parser
2777
- };
2778
- };
2779
- function isValibotWrapper(schema) {
2780
- return schema?._valibot === true;
2781
- }
2782
- async function validateValibotWrapper(wrapper, data) {
2783
- const result = await wrapper.parser(wrapper.schema, data);
2784
- if (!result.success) {
2785
- throw new ValidationError(result.issues);
2786
- }
2787
- return result.output;
2788
- }
2789
- function isClass(schema) {
2790
- try {
2791
- if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
2792
- return true;
2793
- }
2794
- return typeof schema === "function" && schema.prototype && schema.name;
2795
- } catch {
2796
- return false;
2797
- }
2798
- }
2799
- async function validateClassValidator(schema, data) {
2800
- const object = plainToInstance(schema, data);
2801
- try {
2802
- await validateOrReject(object);
2803
- return object;
2804
- } catch (errors) {
2805
- const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
2806
- property: err.property,
2807
- constraints: err.constraints,
2808
- children: err.children
2809
- })) : errors;
2810
- throw new ValidationError(formattedErrors);
2811
- }
2812
- }
2813
- const safelyGetBody = async (ctx) => {
2814
- const req = ctx.req;
2815
- if (req._bodyParsed) {
2816
- return req._bodyValue;
2817
- }
2818
- try {
2819
- let data;
2820
- if (typeof req.json === "function") {
2821
- data = await req.json();
2822
- } else {
2823
- data = req.body;
2824
- if (typeof data === "string") {
2825
- try {
2826
- data = JSON.parse(data);
2827
- } catch {
2828
- }
2829
- }
2830
- }
2831
- req._bodyParsed = true;
2832
- req._bodyValue = data;
2833
- Object.defineProperty(req, "json", {
2834
- value: async () => req._bodyValue,
2835
- configurable: true
2836
- });
2837
- return data;
2838
- } catch (e) {
2839
- return {};
2840
- }
2841
- };
2842
- function validate(config) {
2843
- return async (ctx, next) => {
2844
- const dataToValidate = {};
2845
- if (config.params) dataToValidate.params = ctx.params;
2846
- let queryObj;
2847
- if (config.query) {
2848
- const url = new URL(ctx.req.url);
2849
- queryObj = Object.fromEntries(url.searchParams.entries());
2850
- dataToValidate.query = queryObj;
2851
- }
2852
- if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
2853
- let body;
2854
- if (config.body) {
2855
- body = await safelyGetBody(ctx);
2856
- dataToValidate.body = body;
2857
- }
2858
- if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2859
- await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2860
- }
2861
- if (config.params) {
2862
- ctx.params = await runValidation(config.params, ctx.params);
2863
- }
2864
- let validQuery;
2865
- if (config.query && queryObj) {
2866
- validQuery = await runValidation(config.query, queryObj);
2867
- }
2868
- if (config.headers) {
2869
- const headersObj = Object.fromEntries(ctx.req.headers.entries());
2870
- await runValidation(config.headers, headersObj);
2871
- }
2872
- let validBody;
2873
- if (config.body) {
2874
- const b = body ?? await safelyGetBody(ctx);
2875
- validBody = await runValidation(config.body, b);
2876
- const req = ctx.req;
2877
- req._bodyValue = validBody;
2878
- Object.defineProperty(req, "json", {
2879
- value: async () => validBody,
2880
- configurable: true
2881
- });
2882
- ctx.body = validBody;
2883
- }
2884
- if (ctx.app?.applicationConfig.hooks?.afterValidate) {
2885
- const validatedData = { ...dataToValidate };
2886
- if (config.params) validatedData.params = ctx.params;
2887
- if (config.query) validatedData.query = validQuery;
2888
- if (config.body) validatedData.body = validBody;
2889
- await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
2890
- }
2891
- return next();
2892
- };
2893
- }
2894
- async function runValidation(schema, data) {
2895
- if (isZod(schema)) {
2896
- return validateZod(schema, data);
2897
- }
2898
- if (isTypeBox(schema)) {
2899
- return validateTypeBox(schema, data);
2900
- }
2901
- if (isAjv(schema)) {
2902
- return validateAjv(schema, data);
2903
- }
2904
- if (isValibotWrapper(schema)) {
2905
- return validateValibotWrapper(schema, data);
2906
- }
2907
- if (isClass(schema)) {
2908
- return validateClassValidator(schema, data);
2909
- }
2910
- if (isTypeBox(schema)) {
2911
- return validateTypeBox(schema, data);
2912
- }
2913
- if (isAjv(schema)) {
2914
- return validateAjv(schema, data);
2915
- }
2916
- if (isValibotWrapper(schema)) {
2917
- return validateValibotWrapper(schema, data);
2918
- }
2919
- if (typeof schema === "function") {
2920
- return schema(data);
2921
- }
2922
- throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
2923
- }
2924
3290
  export {
2925
3291
  $appRoot,
2926
3292
  $childControllers,
@@ -2976,6 +3342,7 @@ export {
2976
3342
  Use,
2977
3343
  ValidationError,
2978
3344
  compose,
3345
+ openApiValidator,
2979
3346
  useExpress,
2980
3347
  valibot,
2981
3348
  validate