shokupan 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +11 -8
  2. package/dist/cli.cjs +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/context.d.ts +90 -7
  5. package/dist/index.cjs +746 -453
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.js +690 -419
  8. package/dist/index.js.map +1 -1
  9. package/dist/json-parser-B3dnQmCC.js +35 -0
  10. package/dist/json-parser-B3dnQmCC.js.map +1 -0
  11. package/dist/json-parser-COdZ0fqY.cjs +35 -0
  12. package/dist/json-parser-COdZ0fqY.cjs.map +1 -0
  13. package/dist/{openapi-analyzer-z-7AoFRC.cjs → openapi-analyzer-Bei1sVWp.cjs} +33 -16
  14. package/dist/openapi-analyzer-Bei1sVWp.cjs.map +1 -0
  15. package/dist/{openapi-analyzer-D7y6Qa38.js → openapi-analyzer-Ce_7JxZh.js} +33 -16
  16. package/dist/openapi-analyzer-Ce_7JxZh.js.map +1 -0
  17. package/dist/plugins/proxy.d.ts +2 -0
  18. package/dist/plugins/rate-limit.d.ts +1 -0
  19. package/dist/plugins/scalar.d.ts +1 -1
  20. package/dist/router.d.ts +125 -55
  21. package/dist/{server-adapter-BWrEJbKL.js → server-adapter-0xH174zz.js} +4 -2
  22. package/dist/server-adapter-0xH174zz.js.map +1 -0
  23. package/dist/{server-adapter-fVKP60e0.cjs → server-adapter-DFhwlK8e.cjs} +4 -2
  24. package/dist/server-adapter-DFhwlK8e.cjs.map +1 -0
  25. package/dist/shokupan.d.ts +66 -7
  26. package/dist/types.d.ts +63 -3
  27. package/dist/util/datastore.d.ts +6 -0
  28. package/dist/util/json-parser.d.ts +12 -0
  29. package/dist/util/plugin-deps.d.ts +25 -0
  30. package/package.json +73 -13
  31. package/dist/buntest.d.ts +0 -1
  32. package/dist/openapi-analyzer-D7y6Qa38.js.map +0 -1
  33. package/dist/openapi-analyzer-z-7AoFRC.cjs.map +0 -1
  34. package/dist/server-adapter-BWrEJbKL.js.map +0 -1
  35. package/dist/server-adapter-fVKP60e0.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { Eta } from "eta";
3
3
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
4
- import { resolve, join, basename } from "path";
4
+ import { resolve, join, sep, basename } from "path";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
- import { createNodeEngines } from "@surrealdb/node";
7
- import { Surreal, RecordId } from "surrealdb";
8
6
  import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
9
7
  import * as os from "node:os";
10
8
  import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
@@ -12,9 +10,7 @@ import * as jose from "jose";
12
10
  import * as zlib from "node:zlib";
13
11
  import Ajv from "ajv";
14
12
  import addFormats from "ajv-formats";
15
- import { plainToInstance } from "class-transformer";
16
- import { validateOrReject } from "class-validator";
17
- import { OpenAPIAnalyzer } from "./openapi-analyzer-D7y6Qa38.js";
13
+ import { OpenAPIAnalyzer } from "./openapi-analyzer-Ce_7JxZh.js";
18
14
  import { randomUUID, createHmac } from "crypto";
19
15
  import { EventEmitter } from "events";
20
16
  class ShokupanResponse {
@@ -80,8 +76,82 @@ class ShokupanResponse {
80
76
  return this._headers !== null;
81
77
  }
82
78
  }
79
+ function isValidCookieDomain(domain, currentHost) {
80
+ const hostWithoutPort = currentHost.split(":")[0];
81
+ if (domain === hostWithoutPort) return true;
82
+ if (domain.startsWith(".")) {
83
+ const domainWithoutDot = domain.slice(1);
84
+ return hostWithoutPort.endsWith(domainWithoutDot);
85
+ }
86
+ return false;
87
+ }
88
+ const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
89
+ 100,
90
+ 101,
91
+ 102,
92
+ 103,
93
+ 200,
94
+ 201,
95
+ 202,
96
+ 203,
97
+ 204,
98
+ 205,
99
+ 206,
100
+ 207,
101
+ 208,
102
+ 226,
103
+ 300,
104
+ 301,
105
+ 302,
106
+ 303,
107
+ 304,
108
+ 305,
109
+ 306,
110
+ 307,
111
+ 308,
112
+ 400,
113
+ 401,
114
+ 402,
115
+ 403,
116
+ 404,
117
+ 405,
118
+ 406,
119
+ 407,
120
+ 408,
121
+ 409,
122
+ 410,
123
+ 411,
124
+ 412,
125
+ 413,
126
+ 414,
127
+ 415,
128
+ 416,
129
+ 417,
130
+ 418,
131
+ 421,
132
+ 422,
133
+ 423,
134
+ 424,
135
+ 425,
136
+ 426,
137
+ 428,
138
+ 429,
139
+ 431,
140
+ 451,
141
+ 500,
142
+ 501,
143
+ 502,
144
+ 503,
145
+ 504,
146
+ 505,
147
+ 506,
148
+ 507,
149
+ 508,
150
+ 510,
151
+ 511
152
+ ]);
153
+ const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
83
154
  class ShokupanContext {
84
- // Raw body for compression optimization
85
155
  constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
86
156
  this.request = request;
87
157
  this.server = server;
@@ -104,7 +174,6 @@ class ShokupanContext {
104
174
  }
105
175
  this.response = new ShokupanResponse();
106
176
  }
107
- _url;
108
177
  params = {};
109
178
  // Router assigns this, but default to empty object
110
179
  state;
@@ -113,6 +182,19 @@ class ShokupanContext {
113
182
  _debug;
114
183
  _finalResponse;
115
184
  _rawBody;
185
+ // Raw body for compression optimization
186
+ // Body caching to avoid double parsing
187
+ _url;
188
+ _cachedBody;
189
+ _bodyType;
190
+ _bodyParsed = false;
191
+ _bodyParseError;
192
+ // Cached URL properties to avoid repeated parsing
193
+ _cachedHostname;
194
+ _cachedProtocol;
195
+ _cachedHost;
196
+ _cachedOrigin;
197
+ _cachedQuery;
116
198
  get url() {
117
199
  if (!this._url) {
118
200
  const urlString = this.request.url || "http://localhost/";
@@ -161,16 +243,24 @@ class ShokupanContext {
161
243
  * Request query params
162
244
  */
163
245
  get query() {
164
- const q = {};
165
- for (const [key, value] of this.url.searchParams) {
166
- if (q[key] === void 0) {
167
- q[key] = value;
168
- } else if (Array.isArray(q[key])) {
169
- q[key].push(value);
246
+ if (this._cachedQuery) return this._cachedQuery;
247
+ const q = /* @__PURE__ */ Object.create(null);
248
+ const blocklist = ["__proto__", "constructor", "prototype"];
249
+ const entries = Object.entries(this.url.searchParams);
250
+ for (let i = 0; i < entries.length; i++) {
251
+ const [key, value] = entries[i];
252
+ if (blocklist.includes(key)) continue;
253
+ if (Object.prototype.hasOwnProperty.call(q, key)) {
254
+ if (Array.isArray(q[key])) {
255
+ q[key].push(value);
256
+ } else {
257
+ q[key] = [q[key], value];
258
+ }
170
259
  } else {
171
- q[key] = [q[key], value];
260
+ q[key] = value;
172
261
  }
173
262
  }
263
+ this._cachedQuery = q;
174
264
  return q;
175
265
  }
176
266
  /**
@@ -183,31 +273,31 @@ class ShokupanContext {
183
273
  * Request hostname (e.g. "localhost")
184
274
  */
185
275
  get hostname() {
186
- return this.url.hostname;
276
+ return this._cachedHostname ??= this.url.hostname;
187
277
  }
188
278
  /**
189
279
  * Request host (e.g. "localhost:3000")
190
280
  */
191
281
  get host() {
192
- return this.url.host;
282
+ return this._cachedHost ??= this.url.host;
193
283
  }
194
284
  /**
195
285
  * Request protocol (e.g. "http:", "https:")
196
286
  */
197
287
  get protocol() {
198
- return this.url.protocol;
288
+ return this._cachedProtocol ??= this.url.protocol;
199
289
  }
200
290
  /**
201
291
  * Whether request is secure (https)
202
292
  */
203
293
  get secure() {
204
- return this.url.protocol === "https:";
294
+ return this.protocol === "https:";
205
295
  }
206
296
  /**
207
297
  * Request origin (e.g. "http://localhost:3000")
208
298
  */
209
299
  get origin() {
210
- return this.url.origin;
300
+ return this._cachedOrigin ??= this.url.origin;
211
301
  }
212
302
  /**
213
303
  * Request headers
@@ -244,6 +334,12 @@ class ShokupanContext {
244
334
  * @param options Cookie options
245
335
  */
246
336
  setCookie(name, value, options = {}) {
337
+ if (options.domain) {
338
+ const currentHost = this.hostname;
339
+ if (!isValidCookieDomain(options.domain, currentHost)) {
340
+ throw new Error(`Invalid cookie domain: ${options.domain} for host ${currentHost}`);
341
+ }
342
+ }
247
343
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
248
344
  if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
249
345
  if (options.domain) cookie += `; Domain=${options.domain}`;
@@ -303,6 +399,91 @@ class ShokupanContext {
303
399
  }
304
400
  return h;
305
401
  }
402
+ /**
403
+ * Read request body with caching to avoid double parsing.
404
+ * The body is only parsed once and cached for subsequent reads.
405
+ */
406
+ async body() {
407
+ if (this._bodyParseError) {
408
+ throw this._bodyParseError;
409
+ }
410
+ if (this._bodyParsed) {
411
+ return this._cachedBody;
412
+ }
413
+ const contentType = this.request.headers.get("content-type") || "";
414
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
415
+ const rawText = await this.readRawBody();
416
+ const parserType = this.app?.applicationConfig?.jsonParser || "native";
417
+ if (parserType === "native") {
418
+ this._cachedBody = JSON.parse(rawText);
419
+ } else {
420
+ const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
421
+ const parser = getJSONParser(parserType);
422
+ this._cachedBody = parser(rawText);
423
+ }
424
+ this._bodyType = "json";
425
+ } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
426
+ this._cachedBody = await this.request.formData();
427
+ this._bodyType = "formData";
428
+ } else {
429
+ this._cachedBody = await this.readRawBody();
430
+ this._bodyType = "text";
431
+ }
432
+ this._bodyParsed = true;
433
+ return this._cachedBody;
434
+ }
435
+ /**
436
+ * Pre-parse the request body before handler execution.
437
+ * This improves performance and enables Node.js compatibility for large payloads.
438
+ * Errors are deferred until the body is actually accessed in the handler.
439
+ */
440
+ async parseBody() {
441
+ if (this._bodyParsed) {
442
+ return;
443
+ }
444
+ if (this.request.method === "GET" || this.request.method === "HEAD") {
445
+ return;
446
+ }
447
+ try {
448
+ await this.body();
449
+ } catch (error) {
450
+ this._bodyParseError = error;
451
+ }
452
+ }
453
+ /**
454
+ * Read raw body from ReadableStream efficiently.
455
+ * This is much faster than request.text() for large payloads.
456
+ * Also handles the case where body is already a string (e.g., in tests).
457
+ */
458
+ async readRawBody() {
459
+ if (typeof this.request.body === "string") {
460
+ return this.request.body;
461
+ }
462
+ const reader = this.request.body?.getReader();
463
+ if (!reader) {
464
+ return "";
465
+ }
466
+ const chunks = [];
467
+ let totalSize = 0;
468
+ try {
469
+ while (true) {
470
+ const { done, value } = await reader.read();
471
+ if (done) break;
472
+ chunks.push(value);
473
+ totalSize += value.length;
474
+ }
475
+ } finally {
476
+ reader.releaseLock();
477
+ }
478
+ const result = new Uint8Array(totalSize);
479
+ let offset = 0;
480
+ for (let i = 0; i < chunks.length; i++) {
481
+ const chunk = chunks[i];
482
+ result.set(chunk, offset);
483
+ offset += chunk.length;
484
+ }
485
+ return new TextDecoder().decode(result);
486
+ }
306
487
  /**
307
488
  * Send a response
308
489
  * @param body Response body
@@ -311,31 +492,24 @@ class ShokupanContext {
311
492
  */
312
493
  send(body, options) {
313
494
  const headers = this.mergeHeaders(options?.headers);
314
- const status = options?.status ?? this.response.status;
495
+ const status = options?.status ?? this.response.status ?? 200;
496
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
497
+ throw new Error(`Invalid HTTP status code: ${status}`);
498
+ }
315
499
  if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
316
500
  this._rawBody = body;
317
501
  }
318
502
  this._finalResponse = new Response(body, { status, headers });
319
503
  return this._finalResponse;
320
504
  }
321
- /**
322
- * Read request body
323
- */
324
- async body() {
325
- const contentType = this.request.headers.get("content-type") || "";
326
- if (contentType.includes("application/json") || contentType.includes("+json")) {
327
- return this.request.json();
328
- }
329
- if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
330
- return this.request.formData();
331
- }
332
- return this.request.text();
333
- }
334
505
  /**
335
506
  * Respond with a JSON object
336
507
  */
337
508
  json(data, status, headers) {
338
- const finalStatus = status ?? this.response.status;
509
+ const finalStatus = status ?? this.response.status ?? 200;
510
+ if (!VALID_HTTP_STATUSES.has(finalStatus)) {
511
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
512
+ }
339
513
  const jsonString = JSON.stringify(data);
340
514
  this._rawBody = jsonString;
341
515
  if (!headers && !this.response.hasPopulatedHeaders) {
@@ -354,7 +528,10 @@ class ShokupanContext {
354
528
  * Respond with a text string
355
529
  */
356
530
  text(data, status, headers) {
357
- const finalStatus = status ?? this.response.status;
531
+ const finalStatus = status ?? this.response.status ?? 200;
532
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
533
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
534
+ }
358
535
  this._rawBody = data;
359
536
  if (!headers && !this.response.hasPopulatedHeaders) {
360
537
  this._finalResponse = new Response(data, {
@@ -372,7 +549,10 @@ class ShokupanContext {
372
549
  * Respond with HTML content
373
550
  */
374
551
  html(html, status, headers) {
375
- const finalStatus = status ?? this.response.status;
552
+ const finalStatus = status ?? this.response.status ?? 200;
553
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
554
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
555
+ }
376
556
  const finalHeaders = this.mergeHeaders(headers);
377
557
  finalHeaders.set("content-type", "text/html; charset=utf-8");
378
558
  this._rawBody = html;
@@ -383,6 +563,9 @@ class ShokupanContext {
383
563
  * Respond with a redirect
384
564
  */
385
565
  redirect(url, status = 302) {
566
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
567
+ throw new Error(`Invalid redirect status code: ${status}`);
568
+ }
386
569
  const headers = this.mergeHeaders();
387
570
  headers.set("Location", url);
388
571
  this._finalResponse = new Response(null, { status, headers });
@@ -393,6 +576,9 @@ class ShokupanContext {
393
576
  * DOES NOT CHAIN!
394
577
  */
395
578
  status(status) {
579
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
580
+ throw new Error(`Invalid HTTP status code: ${status}`);
581
+ }
396
582
  const headers = this.mergeHeaders();
397
583
  this._finalResponse = new Response(null, { status, headers });
398
584
  return this._finalResponse;
@@ -403,6 +589,9 @@ class ShokupanContext {
403
589
  async file(path, fileOptions, responseOptions) {
404
590
  const headers = this.mergeHeaders(responseOptions?.headers);
405
591
  const status = responseOptions?.status ?? this.response.status;
592
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
593
+ throw new Error(`Invalid HTTP status code: ${status}`);
594
+ }
406
595
  if (typeof Bun !== "undefined") {
407
596
  this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
408
597
  return this._finalResponse;
@@ -426,6 +615,10 @@ class ShokupanContext {
426
615
  * @param headers HTTP Headers
427
616
  */
428
617
  async jsx(element, args, status, headers) {
618
+ status ??= 200;
619
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
620
+ throw new Error(`Invalid HTTP status code: ${status}`);
621
+ }
429
622
  if (!this.renderer) {
430
623
  throw new Error("No JSX renderer configured");
431
624
  }
@@ -440,17 +633,32 @@ function RateLimitMiddleware(options = {}) {
440
633
  const statusCode = options.statusCode || 429;
441
634
  const headers = options.headers !== false;
442
635
  const mode = options.mode || "user";
636
+ const trustedProxies = options.trustedProxies || [];
443
637
  const keyGenerator = options.keyGenerator || ((ctx) => {
444
638
  if (mode === "absolute") {
445
639
  return "global";
446
640
  }
447
- return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
641
+ const xForwardedFor = ctx.headers.get("x-forwarded-for");
642
+ if (xForwardedFor && trustedProxies.length > 0) {
643
+ const ips = xForwardedFor.split(",").map((ip) => ip.trim());
644
+ for (let i = ips.length - 1; i >= 0; i--) {
645
+ const ip = ips[i];
646
+ if (!trustedProxies.includes(ip)) {
647
+ if (/^[\d.:a-fA-F]+$/.test(ip)) {
648
+ return ip;
649
+ }
650
+ }
651
+ }
652
+ }
653
+ return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
448
654
  });
449
655
  const skip = options.skip || (() => false);
450
656
  const hits = /* @__PURE__ */ new Map();
451
657
  const interval = setInterval(() => {
452
658
  const now = Date.now();
453
- for (const [key, record] of hits.entries()) {
659
+ const entries = Array.from(hits.entries());
660
+ for (let i = 0; i < entries.length; i++) {
661
+ const [key, record] = entries[i];
454
662
  if (record.resetTime <= now) {
455
663
  hits.delete(key);
456
664
  }
@@ -703,7 +911,9 @@ function deepMerge(target, ...sources) {
703
911
  if (!sources.length) return target;
704
912
  const source = sources.shift();
705
913
  if (isObject(target) && isObject(source)) {
706
- for (const key in source) {
914
+ const sourceKeys = Object.keys(source);
915
+ for (let i = 0; i < sourceKeys.length; i++) {
916
+ const key = sourceKeys[i];
707
917
  if (isObject(source[key])) {
708
918
  if (!target[key]) Object.assign(target, { [key]: {} });
709
919
  deepMerge(target[key], source[key]);
@@ -727,15 +937,17 @@ function deepMerge(target, ...sources) {
727
937
  }
728
938
  return deepMerge(target, ...sources);
729
939
  }
730
- const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
731
- const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
732
- const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
733
- const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
734
- const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
735
- const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
736
- const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
737
- const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
738
- const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
940
+ const REGEX_PATTERNS = {
941
+ QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
942
+ QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
943
+ QUERY_NUMBER: /Number\(ctx\.query\.(\w+)\)/g,
944
+ QUERY_BOOL: /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g,
945
+ QUERY_GENERIC: /ctx\.query\.(\w+)/g,
946
+ PARAM_INT: /parseInt\(ctx\.params\.(\w+)\)/g,
947
+ PARAM_FLOAT: /parseFloat\(ctx\.params\.(\w+)\)/g,
948
+ HEADER_GET: /ctx\.get\(['"](\w+)['"]\)/g,
949
+ ERROR_STATUS: /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g
950
+ };
739
951
  function analyzeHandler(handler) {
740
952
  const handlerSource = handler.toString();
741
953
  const inferredSpec = {};
@@ -745,29 +957,20 @@ function analyzeHandler(handler) {
745
957
  };
746
958
  }
747
959
  const queryParams = /* @__PURE__ */ new Map();
748
- for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
749
- if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
750
- }
751
- for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
752
- if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
753
- }
754
- for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
755
- if (match[1] && !queryParams.has(match[1])) {
756
- queryParams.set(match[1], { type: "number" });
757
- }
758
- }
759
- for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
760
- const name = match[1] || match[2];
761
- if (name && !queryParams.has(name)) {
762
- queryParams.set(name, { type: "boolean" });
763
- }
764
- }
765
- for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
766
- const name = match[1];
767
- if (name && !queryParams.has(name)) {
768
- queryParams.set(name, { type: "string" });
960
+ const processMatches = (regex, type, format) => {
961
+ const matches = Array.from(handlerSource.matchAll(regex));
962
+ for (const match of matches) {
963
+ const name = match[1] || match[2];
964
+ if (name && !queryParams.has(name)) {
965
+ queryParams.set(name, { type, format });
966
+ }
769
967
  }
770
- }
968
+ };
969
+ processMatches(REGEX_PATTERNS.QUERY_INT, "integer", "int32");
970
+ processMatches(REGEX_PATTERNS.QUERY_FLOAT, "number", "float");
971
+ processMatches(REGEX_PATTERNS.QUERY_NUMBER, "number");
972
+ processMatches(REGEX_PATTERNS.QUERY_BOOL, "boolean");
973
+ processMatches(REGEX_PATTERNS.QUERY_GENERIC, "string");
771
974
  if (queryParams.size > 0) {
772
975
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
773
976
  queryParams.forEach((schema, paramName) => {
@@ -779,12 +982,15 @@ function analyzeHandler(handler) {
779
982
  });
780
983
  }
781
984
  const pathParams = /* @__PURE__ */ new Map();
782
- for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
783
- if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
784
- }
785
- for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
786
- if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
787
- }
985
+ const processPathMatches = (regex, type, format) => {
986
+ const matches = Array.from(handlerSource.matchAll(regex));
987
+ for (const match of matches) {
988
+ const name = match[1];
989
+ if (name) pathParams.set(name, { type, format });
990
+ }
991
+ };
992
+ processPathMatches(REGEX_PATTERNS.PARAM_INT, "integer", "int32");
993
+ processPathMatches(REGEX_PATTERNS.PARAM_FLOAT, "number", "float");
788
994
  if (pathParams.size > 0) {
789
995
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
790
996
  pathParams.forEach((schema, paramName) => {
@@ -796,7 +1002,8 @@ function analyzeHandler(handler) {
796
1002
  });
797
1003
  });
798
1004
  }
799
- for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
1005
+ const headerMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.HEADER_GET));
1006
+ for (const match of headerMatches) {
800
1007
  if (match[1]) {
801
1008
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
802
1009
  inferredSpec.parameters.push({
@@ -815,13 +1022,19 @@ function analyzeHandler(handler) {
815
1022
  }
816
1023
  if (handlerSource.includes("ctx.html(")) {
817
1024
  responses["200"] = {
818
- description: "Successful response",
1025
+ description: "Successful HTML response",
1026
+ content: { "text/html": { schema: { type: "string" } } }
1027
+ };
1028
+ }
1029
+ if (handlerSource.includes("ctx.jsx(")) {
1030
+ responses["200"] = {
1031
+ description: "Successful HTML response (Rendered JSX)",
819
1032
  content: { "text/html": { schema: { type: "string" } } }
820
1033
  };
821
1034
  }
822
1035
  if (handlerSource.includes("ctx.text(")) {
823
1036
  responses["200"] = {
824
- description: "Successful response",
1037
+ description: "Successful text response",
825
1038
  content: { "text/plain": { schema: { type: "string" } } }
826
1039
  };
827
1040
  }
@@ -832,7 +1045,18 @@ function analyzeHandler(handler) {
832
1045
  };
833
1046
  }
834
1047
  if (handlerSource.includes("ctx.redirect(")) {
835
- responses["302"] = { description: "Redirect" };
1048
+ let hasSpecificRedirect = false;
1049
+ const redirectMatches = Array.from(handlerSource.matchAll(/ctx\.redirect\([^,]+,\s*(\d{3})\)/g));
1050
+ for (const match of redirectMatches) {
1051
+ const status = match[1];
1052
+ if (/^30[12378]$/.test(status)) {
1053
+ responses[status] = { description: `Redirect (${status})` };
1054
+ hasSpecificRedirect = true;
1055
+ }
1056
+ }
1057
+ if (!hasSpecificRedirect) {
1058
+ responses["302"] = { description: "Redirect" };
1059
+ }
836
1060
  }
837
1061
  if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
838
1062
  responses["200"] = {
@@ -840,7 +1064,8 @@ function analyzeHandler(handler) {
840
1064
  content: { "application/json": { schema: { type: "object" } } }
841
1065
  };
842
1066
  }
843
- for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
1067
+ const errorStatusMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.ERROR_STATUS));
1068
+ for (const match of errorStatusMatches) {
844
1069
  const statusCode = match[1];
845
1070
  if (statusCode && statusCode !== "200") {
846
1071
  responses[statusCode] = { description: `Error response (${statusCode})` };
@@ -851,6 +1076,52 @@ function analyzeHandler(handler) {
851
1076
  }
852
1077
  return { inferredSpec };
853
1078
  }
1079
+ async function getAstRoutes(applications) {
1080
+ const astRoutes = [];
1081
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
1082
+ if (seen.has(app.name)) return [];
1083
+ const newSeen = new Set(seen);
1084
+ newSeen.add(app.name);
1085
+ const expanded = [];
1086
+ for (const route of app.routes) {
1087
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1088
+ const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1089
+ let joined = cleanPrefix + cleanPath;
1090
+ if (joined.length > 1 && joined.endsWith("/")) {
1091
+ joined = joined.slice(0, -1);
1092
+ }
1093
+ expanded.push({
1094
+ ...route,
1095
+ path: joined || "/"
1096
+ });
1097
+ }
1098
+ if (app.mounted) {
1099
+ for (const mount of app.mounted) {
1100
+ const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
1101
+ if (targetApp) {
1102
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1103
+ const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
1104
+ expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
1105
+ }
1106
+ }
1107
+ }
1108
+ return expanded;
1109
+ };
1110
+ applications.forEach((app) => {
1111
+ astRoutes.push(...getExpandedRoutes(app));
1112
+ });
1113
+ const dedupedRoutes = /* @__PURE__ */ new Map();
1114
+ for (const route of astRoutes) {
1115
+ const key = `${route.method.toUpperCase()}:${route.path}`;
1116
+ let score = 0;
1117
+ if (route.responseSchema) score += 10;
1118
+ if (route.handlerSource) score += 5;
1119
+ if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
1120
+ dedupedRoutes.set(key, { route, score });
1121
+ }
1122
+ }
1123
+ return Array.from(dedupedRoutes.values()).map((v) => v.route);
1124
+ }
854
1125
  async function generateOpenApi(rootRouter, options = {}) {
855
1126
  const paths = {};
856
1127
  const tagGroups = /* @__PURE__ */ new Map();
@@ -858,61 +1129,11 @@ async function generateOpenApi(rootRouter, options = {}) {
858
1129
  const defaultTagName = options.defaultTag || "Application";
859
1130
  let astRoutes = [];
860
1131
  try {
861
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-D7y6Qa38.js");
1132
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-Ce_7JxZh.js");
862
1133
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
863
1134
  const { applications } = await analyzer.analyze();
864
- const appMap = /* @__PURE__ */ new Map();
865
- applications.forEach((app) => {
866
- appMap.set(app.name, app);
867
- if (app.name !== app.className) {
868
- appMap.set(app.className, app);
869
- }
870
- });
871
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
872
- if (seen.has(app.name)) return [];
873
- const newSeen = new Set(seen);
874
- newSeen.add(app.name);
875
- const expanded = [];
876
- for (const route of app.routes) {
877
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
878
- const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
879
- let joined = cleanPrefix + cleanPath;
880
- if (joined.length > 1 && joined.endsWith("/")) {
881
- joined = joined.slice(0, -1);
882
- }
883
- expanded.push({
884
- ...route,
885
- path: joined || "/"
886
- });
887
- }
888
- if (app.mounted) {
889
- for (const mount of app.mounted) {
890
- const targetApp = appMap.get(mount.target);
891
- if (targetApp) {
892
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
893
- const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
894
- expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
895
- }
896
- }
897
- }
898
- return expanded;
899
- };
900
- applications.forEach((app) => {
901
- astRoutes.push(...getExpandedRoutes(app));
902
- });
903
- const dedupedRoutes = /* @__PURE__ */ new Map();
904
- for (const route of astRoutes) {
905
- const key = `${route.method.toUpperCase()}:${route.path}`;
906
- let score = 0;
907
- if (route.responseSchema) score += 10;
908
- if (route.handlerSource) score += 5;
909
- if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
910
- dedupedRoutes.set(key, { route, score });
911
- }
912
- }
913
- astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
1135
+ astRoutes = await getAstRoutes(applications);
914
1136
  } catch (e) {
915
- console.warn("OpenAPI AST analysis failed or skipped:", e);
916
1137
  }
917
1138
  const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
918
1139
  let group = currentGroup;
@@ -969,33 +1190,15 @@ async function generateOpenApi(rootRouter, options = {}) {
969
1190
  (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
970
1191
  );
971
1192
  if (!astMatch) {
972
- let runtimeSource = route.handler.toString();
973
- if (route.handler.originalHandler) {
974
- runtimeSource = route.handler.originalHandler.toString();
975
- }
1193
+ const runtimeSource = (route.handler.originalHandler || route.handler).toString();
976
1194
  const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
977
1195
  const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
978
1196
  astMatch = sameMethodRoutes.find((r) => {
979
1197
  const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
980
1198
  if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
981
- const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
982
- return match;
1199
+ return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
983
1200
  });
984
1201
  }
985
- const potentialMatches = astRoutes.filter(
986
- (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
987
- );
988
- if (potentialMatches.length > 1) {
989
- const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
990
- const preciseMatch = potentialMatches.find((r) => {
991
- const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
992
- const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
993
- return match;
994
- });
995
- if (preciseMatch) {
996
- astMatch = preciseMatch;
997
- }
998
- }
999
1202
  if (astMatch) {
1000
1203
  if (astMatch.summary) operation.summary = astMatch.summary;
1001
1204
  if (astMatch.description) operation.description = astMatch.description;
@@ -1003,25 +1206,19 @@ async function generateOpenApi(rootRouter, options = {}) {
1003
1206
  if (astMatch.operationId) operation.operationId = astMatch.operationId;
1004
1207
  if (astMatch.requestTypes?.body) {
1005
1208
  operation.requestBody = {
1006
- content: {
1007
- "application/json": { schema: astMatch.requestTypes.body }
1008
- }
1209
+ content: { "application/json": { schema: astMatch.requestTypes.body } }
1009
1210
  };
1010
1211
  }
1011
1212
  if (astMatch.responseSchema) {
1012
1213
  operation.responses["200"] = {
1013
1214
  description: "Successful response",
1014
- content: {
1015
- "application/json": { schema: astMatch.responseSchema }
1016
- }
1215
+ content: { "application/json": { schema: astMatch.responseSchema } }
1017
1216
  };
1018
1217
  } else if (astMatch.responseType) {
1019
1218
  const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
1020
1219
  operation.responses["200"] = {
1021
1220
  description: "Successful response",
1022
- content: {
1023
- [contentType]: { schema: { type: astMatch.responseType } }
1024
- }
1221
+ content: { [contentType]: { schema: { type: astMatch.responseType } } }
1025
1222
  };
1026
1223
  }
1027
1224
  const params = [];
@@ -1072,15 +1269,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1072
1269
  deepMerge(operation, inferredSpec);
1073
1270
  }
1074
1271
  if (route.handlerSpec) {
1075
- const spec = route.handlerSpec;
1076
- if (spec.summary) operation.summary = spec.summary;
1077
- if (spec.description) operation.description = spec.description;
1078
- if (spec.operationId) operation.operationId = spec.operationId;
1079
- if (spec.tags) operation.tags = spec.tags;
1080
- if (spec.security) operation.security = spec.security;
1081
- if (spec.responses) {
1082
- operation.responses = { ...operation.responses, ...spec.responses };
1083
- }
1272
+ deepMerge(operation, route.handlerSpec);
1084
1273
  }
1085
1274
  if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
1086
1275
  if (operation.tags) {
@@ -1099,11 +1288,13 @@ async function generateOpenApi(rootRouter, options = {}) {
1099
1288
  paths[fullPath][methodLower] = operation;
1100
1289
  }
1101
1290
  }
1102
- for (const controller of router[$childControllers]) {
1291
+ const controllers = router[$childControllers];
1292
+ for (const controller of controllers) {
1103
1293
  const controllerName = controller.constructor.name || "UnknownController";
1104
1294
  tagGroups.get(group)?.add(controllerName);
1105
1295
  }
1106
- for (const child of router[$childRouters]) {
1296
+ const childRouters = router[$childRouters];
1297
+ for (const child of childRouters) {
1107
1298
  const mountPath = child[$mountPath];
1108
1299
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1109
1300
  const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
@@ -1113,7 +1304,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1113
1304
  };
1114
1305
  collect(rootRouter);
1115
1306
  const xTagGroups = [];
1116
- for (const [name, tags] of tagGroups) {
1307
+ for (const [name, tags] of tagGroups.entries()) {
1117
1308
  xTagGroups.push({ name, tags: Array.from(tags).sort() });
1118
1309
  }
1119
1310
  return {
@@ -1135,12 +1326,23 @@ function serveStatic(config, prefix) {
1135
1326
  let relative = ctx.path.slice(normalizedPrefix.length);
1136
1327
  if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
1137
1328
  if (relative.length === 0) relative = "/";
1138
- relative = decodeURIComponent(relative);
1139
- const requestPath = join(rootPath, relative);
1140
- if (!requestPath.startsWith(rootPath)) {
1329
+ if (relative.includes("\0")) {
1330
+ return ctx.json({ error: "Forbidden" }, 403);
1331
+ }
1332
+ try {
1333
+ relative = decodeURIComponent(relative);
1334
+ } catch (e) {
1335
+ return ctx.json({ error: "Bad Request" }, 400);
1336
+ }
1337
+ if (relative.includes("\0")) {
1338
+ return ctx.json({ error: "Forbidden" }, 403);
1339
+ }
1340
+ if (relative.includes("../") || relative.includes("..\\")) {
1141
1341
  return ctx.json({ error: "Forbidden" }, 403);
1142
1342
  }
1143
- if (requestPath.includes("\0")) {
1343
+ const requestPath = resolve(join(rootPath, relative));
1344
+ const normalizedRoot = resolve(rootPath);
1345
+ if (!requestPath.startsWith(normalizedRoot + sep) && requestPath !== normalizedRoot) {
1144
1346
  return ctx.json({ error: "Forbidden" }, 403);
1145
1347
  }
1146
1348
  if (config.hooks?.onRequest) {
@@ -1148,7 +1350,8 @@ function serveStatic(config, prefix) {
1148
1350
  if (res) return res;
1149
1351
  }
1150
1352
  if (config.exclude) {
1151
- for (const pattern of config.exclude) {
1353
+ for (let i = 0; i < config.exclude.length; i++) {
1354
+ const pattern = config.exclude[i];
1152
1355
  if (pattern instanceof RegExp) {
1153
1356
  if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
1154
1357
  } else if (typeof pattern === "string") {
@@ -1167,7 +1370,8 @@ function serveStatic(config, prefix) {
1167
1370
  stats = await stat(requestPath);
1168
1371
  } catch (e) {
1169
1372
  if (config.extensions) {
1170
- for (const ext of config.extensions) {
1373
+ for (let i = 0; i < config.extensions.length; i++) {
1374
+ const ext = config.extensions[i];
1171
1375
  const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
1172
1376
  try {
1173
1377
  const s = await stat(p);
@@ -1196,7 +1400,8 @@ function serveStatic(config, prefix) {
1196
1400
  indexes = [config.index];
1197
1401
  }
1198
1402
  let foundIndex = false;
1199
- for (const idx of indexes) {
1403
+ for (let i = 0; i < indexes.length; i++) {
1404
+ const idx = indexes[i];
1200
1405
  const idxPath = join(finalPath, idx);
1201
1406
  try {
1202
1407
  const idxStats = await stat(idxPath);
@@ -1278,7 +1483,8 @@ class RouterTrie {
1278
1483
  insert(method, path, handler) {
1279
1484
  let node = this.root;
1280
1485
  const segments = this.splitPath(path);
1281
- for (const segment of segments) {
1486
+ for (let i = 0; i < segments.length; i++) {
1487
+ const segment = segments[i];
1282
1488
  if (segment === "**") {
1283
1489
  if (!node.recursiveChild) {
1284
1490
  node.recursiveChild = this.createNode();
@@ -1365,40 +1571,68 @@ class RouterTrie {
1365
1571
  }
1366
1572
  }
1367
1573
  const asyncContext = new AsyncLocalStorage();
1368
- const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1369
- const db = new Surreal({
1370
- engines: createNodeEngines()
1371
- });
1372
- const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
1373
- return db.query(`
1374
- DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1375
- DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1376
- DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1377
- DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1378
- DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1379
- DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1380
- `);
1381
- });
1574
+ let db;
1575
+ let dbPromise = null;
1576
+ let RecordId;
1577
+ async function ensureDb() {
1578
+ if (db) return db;
1579
+ if (dbPromise) return dbPromise;
1580
+ dbPromise = (async () => {
1581
+ try {
1582
+ const { createNodeEngines } = await import("@surrealdb/node");
1583
+ const surreal = await import("surrealdb");
1584
+ const Surreal = surreal.Surreal;
1585
+ RecordId = surreal.RecordId;
1586
+ const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1587
+ const _db = new Surreal({
1588
+ engines: createNodeEngines()
1589
+ });
1590
+ await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1591
+ await _db.query(`
1592
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1593
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1594
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1595
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1596
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1597
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1598
+ `);
1599
+ db = _db;
1600
+ return db;
1601
+ } catch (e) {
1602
+ dbPromise = null;
1603
+ if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1604
+ throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1605
+ }
1606
+ throw e;
1607
+ }
1608
+ })();
1609
+ return dbPromise;
1610
+ }
1382
1611
  const datastore = {
1383
- get(store, key) {
1612
+ async get(store, key) {
1613
+ await ensureDb();
1384
1614
  return db.select(new RecordId(store, key));
1385
1615
  },
1386
- set(store, key, value) {
1616
+ async set(store, key, value) {
1617
+ await ensureDb();
1387
1618
  return db.create(new RecordId(store, key)).content(value);
1388
1619
  },
1389
1620
  async query(query, vars) {
1621
+ await ensureDb();
1390
1622
  try {
1391
- const r = await db.query(query, vars).collect();
1392
- return r;
1623
+ const r = await db.query(query, vars);
1624
+ return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
1393
1625
  } catch (e) {
1394
1626
  console.error("DS ERROR:", e);
1395
1627
  throw e;
1396
1628
  }
1397
1629
  },
1398
- ready
1630
+ get ready() {
1631
+ return ensureDb().then(() => void 0);
1632
+ }
1399
1633
  };
1400
1634
  process.on("exit", async () => {
1401
- await db.close();
1635
+ if (db) await db.close();
1402
1636
  });
1403
1637
  const tracer = trace.getTracer("shokupan.middleware");
1404
1638
  function traceHandler(fn, name) {
@@ -1471,6 +1705,8 @@ class ShokupanRouter {
1471
1705
  [$parent] = null;
1472
1706
  [$childRouters] = [];
1473
1707
  [$childControllers] = [];
1708
+ hookCache = /* @__PURE__ */ new Map();
1709
+ hooksInitialized = false;
1474
1710
  middleware = [];
1475
1711
  get rootConfig() {
1476
1712
  return this[$appRoot]?.applicationConfig;
@@ -1488,7 +1724,8 @@ class ShokupanRouter {
1488
1724
  getComponentRegistry() {
1489
1725
  const controllerRoutesMap = /* @__PURE__ */ new Map();
1490
1726
  const localRoutes = [];
1491
- for (const r of this[$routes]) {
1727
+ for (let i = 0; i < this[$routes].length; i++) {
1728
+ const r = this[$routes][i];
1492
1729
  const entry = {
1493
1730
  type: "route",
1494
1731
  path: r.path,
@@ -1625,7 +1862,8 @@ class ShokupanRouter {
1625
1862
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1626
1863
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1627
1864
  let routesAttached = 0;
1628
- for (const name of Array.from(methods)) {
1865
+ for (let i = 0; i < Array.from(methods).length; i++) {
1866
+ const name = Array.from(methods)[i];
1629
1867
  if (name === "constructor") continue;
1630
1868
  if (["arguments", "caller", "callee"].includes(name)) continue;
1631
1869
  const originalHandler = instance[name];
@@ -1637,7 +1875,8 @@ class ShokupanRouter {
1637
1875
  method = config.method;
1638
1876
  subPath = config.path;
1639
1877
  } else {
1640
- for (const m of HTTPMethods) {
1878
+ for (let j = 0; j < HTTPMethods.length; j++) {
1879
+ const m = HTTPMethods[j];
1641
1880
  if (name.toUpperCase().startsWith(m)) {
1642
1881
  method = m;
1643
1882
  const rest = name.slice(m.length);
@@ -1652,8 +1891,8 @@ class ShokupanRouter {
1652
1891
  buffer = "";
1653
1892
  }
1654
1893
  };
1655
- for (let i = 0; i < rest.length; i++) {
1656
- const char = rest[i];
1894
+ for (let i2 = 0; i2 < rest.length; i2++) {
1895
+ const char = rest[i2];
1657
1896
  if (char === "$") {
1658
1897
  flush();
1659
1898
  subPath += "/:";
@@ -1691,7 +1930,8 @@ class ShokupanRouter {
1691
1930
  if (routeArgs?.length > 0) {
1692
1931
  args = [];
1693
1932
  const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1694
- for (const arg of sortedArgs) {
1933
+ for (let k = 0; k < sortedArgs.length; k++) {
1934
+ const arg = sortedArgs[k];
1695
1935
  switch (arg.type) {
1696
1936
  case RouteParamType.BODY:
1697
1937
  try {
@@ -1721,7 +1961,9 @@ class ShokupanRouter {
1721
1961
  args[arg.index] = vals.length > 1 ? vals : vals[0];
1722
1962
  } else {
1723
1963
  const query = {};
1724
- for (const key of url.searchParams.keys()) {
1964
+ const keys = Object.keys(url.searchParams);
1965
+ for (let k2 = 0; k2 < keys.length; k2++) {
1966
+ const key = keys[k2];
1725
1967
  const vals = url.searchParams.getAll(key);
1726
1968
  query[key] = vals.length > 1 ? vals : vals[0];
1727
1969
  }
@@ -1778,9 +2020,11 @@ class ShokupanRouter {
1778
2020
  path: r.path,
1779
2021
  handler: r.handler
1780
2022
  }));
1781
- for (const child of this[$childRouters]) {
2023
+ for (let i = 0; i < this[$childRouters].length; i++) {
2024
+ const child = this[$childRouters][i];
1782
2025
  const childRoutes = child.getRoutes();
1783
- for (const route of childRoutes) {
2026
+ for (let j = 0; j < childRoutes.length; j++) {
2027
+ const route = childRoutes[j];
1784
2028
  const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
1785
2029
  const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1786
2030
  const fullPath = cleanPrefix + cleanPath || "/";
@@ -1794,12 +2038,12 @@ class ShokupanRouter {
1794
2038
  return routes;
1795
2039
  }
1796
2040
  /**
1797
- * Makes a sub request to this router.
1798
- * This is useful for triggering other methods or route handlers.
2041
+ * Makes an internal request through this router's full routing pipeline.
2042
+ * This is useful for calling other routes internally and supports streaming responses.
1799
2043
  * @param options The request options.
1800
- * @returns The response.
2044
+ * @returns The raw Response object.
1801
2045
  */
1802
- async subRequest(arg) {
2046
+ async internalRequest(arg) {
1803
2047
  const options = typeof arg === "string" ? { path: arg } : arg;
1804
2048
  const store = asyncContext.getStore();
1805
2049
  store?.get("req");
@@ -1818,9 +2062,10 @@ class ShokupanRouter {
1818
2062
  return this.root[$dispatch](req);
1819
2063
  }
1820
2064
  /**
1821
- * Processes a request directly.
2065
+ * Processes a request for testing purposes.
2066
+ * Returns a simplified { status, headers, data } object instead of a Response.
1822
2067
  */
1823
- async processRequest(options) {
2068
+ async testRequest(options) {
1824
2069
  let url = options.url || options.path || "/";
1825
2070
  if (!url.startsWith("http")) {
1826
2071
  const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
@@ -1829,7 +2074,9 @@ class ShokupanRouter {
1829
2074
  }
1830
2075
  if (options.query) {
1831
2076
  const u = new URL(url);
1832
- for (const [k, v] of Object.entries(options.query)) {
2077
+ const entries = Object.entries(options.query);
2078
+ for (let i = 0; i < entries.length; i++) {
2079
+ const [k, v] = entries[i];
1833
2080
  u.searchParams.set(k, v);
1834
2081
  }
1835
2082
  url = u.toString();
@@ -1874,28 +2121,17 @@ class ShokupanRouter {
1874
2121
  data: result
1875
2122
  };
1876
2123
  }
1877
- applyRouterHooks(match) {
1878
- if (!this.config?.hooks) return match;
1879
- const hooks = this.config.hooks;
1880
- return {
1881
- ...match,
1882
- handler: this.wrapWithHooks(match.handler, hooks)
1883
- };
1884
- }
1885
- wrapWithHooks(handler, hooks) {
1886
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
1887
- const hasStart = hookList.some((h) => !!h.onRequestStart);
1888
- const hasEnd = hookList.some((h) => !!h.onRequestEnd);
1889
- const hasError = hookList.some((h) => !!h.onError);
2124
+ wrapWithHooks(handler) {
2125
+ if (!this.hooksInitialized) {
2126
+ this.ensureHooksInitialized();
2127
+ }
2128
+ const hasStart = this.hookCache.get("onRequestStart")?.length > 0;
2129
+ const hasEnd = this.hookCache.get("onRequestEnd")?.length > 0;
2130
+ const hasError = this.hookCache.get("onError")?.length > 0;
1890
2131
  if (!hasStart && !hasEnd && !hasError) return handler;
1891
2132
  const originalHandler = handler;
1892
2133
  const wrapped = async (ctx) => {
1893
- if (hasStart) {
1894
- for (let i = 0; i < hookList.length; i++) {
1895
- const h = hookList[i];
1896
- if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
1897
- }
1898
- }
2134
+ await this.runHooks("onRequestStart", ctx);
1899
2135
  const debug = ctx._debug;
1900
2136
  let debugId;
1901
2137
  let previousNode;
@@ -1909,17 +2145,11 @@ class ShokupanRouter {
1909
2145
  try {
1910
2146
  const res = await originalHandler(ctx);
1911
2147
  debug?.trackStep(debugId, "handler", performance.now() - start, "success");
1912
- for (let i = 0; i < hookList.length; i++) {
1913
- const h = hookList[i];
1914
- if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
1915
- }
2148
+ await this.runHooks("onRequestEnd", ctx);
1916
2149
  return res;
1917
2150
  } catch (err) {
1918
2151
  debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
1919
- for (let i = 0; i < hookList.length; i++) {
1920
- const h = hookList[i];
1921
- if (typeof h.onError === "function") await h.onError(err, ctx);
1922
- }
2152
+ await this.runHooks("onError", ctx, err);
1923
2153
  throw err;
1924
2154
  } finally {
1925
2155
  if (debug && previousNode) debug.setNode(previousNode);
@@ -1941,18 +2171,19 @@ class ShokupanRouter {
1941
2171
  result = this.trie.search("GET", path);
1942
2172
  if (result) return result;
1943
2173
  }
1944
- for (const child of this[$childRouters]) {
2174
+ for (let i = 0; i < this[$childRouters].length; i++) {
2175
+ const child = this[$childRouters][i];
1945
2176
  const prefix = child[$mountPath];
1946
2177
  if (path === prefix || path.startsWith(prefix + "/")) {
1947
2178
  const subPath = path.slice(prefix.length) || "/";
1948
2179
  const match = child.find(method, subPath);
1949
- if (match) return this.applyRouterHooks(match);
2180
+ if (match) return match;
1950
2181
  }
1951
2182
  if (prefix.endsWith("/")) {
1952
2183
  if (path.startsWith(prefix)) {
1953
2184
  const subPath = path.slice(prefix.length) || "/";
1954
2185
  const match = child.find(method, subPath);
1955
- if (match) return this.applyRouterHooks(match);
2186
+ if (match) return match;
1956
2187
  }
1957
2188
  }
1958
2189
  }
@@ -1960,10 +2191,13 @@ class ShokupanRouter {
1960
2191
  }
1961
2192
  parsePath(path) {
1962
2193
  const keys = [];
2194
+ if (path.length > 2048) {
2195
+ throw new Error("Path too long");
2196
+ }
1963
2197
  const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
1964
2198
  keys.push(key);
1965
- return "([^/]+)";
1966
- }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
2199
+ return "([^/]{1,255})";
2200
+ }).replace(/\*\*/g, ".{0,1000}").replace(/\*/g, "[^/]{1,255}");
1967
2201
  return {
1968
2202
  regex: new RegExp(`^${pattern}$`),
1969
2203
  keys
@@ -1974,17 +2208,23 @@ class ShokupanRouter {
1974
2208
  /**
1975
2209
  * Adds a route to the router.
1976
2210
  *
1977
- * @param method - HTTP method
1978
- * @param path - URL path
1979
- * @param spec - OpenAPI specification for the route
1980
- * @param handler - Route handler function
1981
- * @param requestTimeout - Timeout for this route in milliseconds
2211
+ * @param arg - Route configuration object
2212
+ * @param arg.method - HTTP method
2213
+ * @param arg.path - URL path
2214
+ * @param arg.spec - OpenAPI specification for the route
2215
+ * @param arg.handler - Route handler function
2216
+ * @param arg.regex - Custom regex for path matching
2217
+ * @param arg.group - Group for the route
2218
+ * @param arg.requestTimeout - Timeout for this route in milliseconds
2219
+ * @param arg.renderer - JSX renderer for the route
2220
+ * @param arg.controller - Controller for the route
1982
2221
  */
1983
2222
  add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
1984
2223
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
1985
2224
  if (this.currentGuards.length > 0) {
1986
2225
  spec = spec || {};
1987
- for (const guard of this.currentGuards) {
2226
+ for (let i = 0; i < this.currentGuards.length; i++) {
2227
+ const guard = this.currentGuards[i];
1988
2228
  if (guard.spec) {
1989
2229
  if (guard.spec.responses) {
1990
2230
  spec.responses = spec.responses || {};
@@ -2013,7 +2253,8 @@ class ShokupanRouter {
2013
2253
  if (routeGuards.length > 0) {
2014
2254
  const innerHandler = wrappedHandler;
2015
2255
  wrappedHandler = async (ctx) => {
2016
- for (const guard of routeGuards) {
2256
+ for (let i = 0; i < routeGuards.length; i++) {
2257
+ const guard = routeGuards[i];
2017
2258
  let guardPassed = false;
2018
2259
  let nextCalled = false;
2019
2260
  const next = () => {
@@ -2107,7 +2348,7 @@ class ShokupanRouter {
2107
2348
  wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2108
2349
  let bakedHandler = wrappedHandler;
2109
2350
  if (this.config?.hooks) {
2110
- bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2351
+ bakedHandler = this.wrapWithHooks(wrappedHandler);
2111
2352
  }
2112
2353
  this[$routes].push({
2113
2354
  method,
@@ -2264,6 +2505,67 @@ class ShokupanRouter {
2264
2505
  generateApiSpec(options = {}) {
2265
2506
  return generateOpenApi(this, options);
2266
2507
  }
2508
+ ensureHooksInitialized() {
2509
+ const hooks = this.config?.hooks;
2510
+ if (hooks) {
2511
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2512
+ const hookTypes = [
2513
+ "onRequestStart",
2514
+ "onRequestEnd",
2515
+ "onResponseStart",
2516
+ "onResponseEnd",
2517
+ "onError",
2518
+ "beforeValidate",
2519
+ "afterValidate",
2520
+ "onRequestTimeout",
2521
+ "onReadTimeout",
2522
+ "onWriteTimeout"
2523
+ ];
2524
+ for (let i = 0; i < hookTypes.length; i++) {
2525
+ const type = hookTypes[i];
2526
+ const fns = [];
2527
+ for (let j = 0; j < hookList.length; j++) {
2528
+ const h = hookList[j];
2529
+ if (h[type]) fns.push(h[type]);
2530
+ }
2531
+ if (fns.length > 0) {
2532
+ this.hookCache.set(type, fns);
2533
+ }
2534
+ }
2535
+ }
2536
+ this.hooksInitialized = true;
2537
+ }
2538
+ async runHooks(name, ...args) {
2539
+ if (!this.hooksInitialized) {
2540
+ this.ensureHooksInitialized();
2541
+ }
2542
+ const fns = this.hookCache.get(name);
2543
+ if (!fns) return;
2544
+ const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2545
+ const debug = ctx?._debug;
2546
+ if (debug) {
2547
+ await Promise.all(fns.map(async (fn, index) => {
2548
+ const hookId = `hook_${name}_${fn.name || index}`;
2549
+ const previousNode = debug.getCurrentNode();
2550
+ debug.trackEdge(previousNode, hookId);
2551
+ debug.setNode(hookId);
2552
+ const start = performance.now();
2553
+ try {
2554
+ await fn(...args);
2555
+ const duration = performance.now() - start;
2556
+ debug.trackStep(hookId, "hook", duration, "success");
2557
+ } catch (error) {
2558
+ const duration = performance.now() - start;
2559
+ debug.trackStep(hookId, "hook", duration, "error", error);
2560
+ throw error;
2561
+ } finally {
2562
+ if (previousNode) debug.setNode(previousNode);
2563
+ }
2564
+ }));
2565
+ } else {
2566
+ await Promise.all(fns.map((fn) => fn(...args)));
2567
+ }
2568
+ }
2267
2569
  }
2268
2570
  class SystemCpuMonitor {
2269
2571
  constructor(intervalMs = 1e3) {
@@ -2321,15 +2623,13 @@ class Shokupan extends ShokupanRouter {
2321
2623
  openApiSpec;
2322
2624
  composedMiddleware;
2323
2625
  cpuMonitor;
2324
- hookCache = /* @__PURE__ */ new Map();
2325
- hooksInitialized = false;
2326
2626
  get logger() {
2327
2627
  return this.applicationConfig.logger;
2328
2628
  }
2329
2629
  constructor(applicationConfig = {}) {
2330
2630
  const config = Object.assign({}, defaults, applicationConfig);
2331
2631
  const { hooks, ...routerConfig } = config;
2332
- super(routerConfig);
2632
+ super({ ...routerConfig, hooks });
2333
2633
  this[$isApplication] = true;
2334
2634
  this[$appRoot] = this;
2335
2635
  this.applicationConfig = config;
@@ -2344,7 +2644,6 @@ class Shokupan extends ShokupanRouter {
2344
2644
  * Adds middleware to the application.
2345
2645
  */
2346
2646
  use(middleware) {
2347
- let trackedMiddleware = middleware;
2348
2647
  const { file, line } = getCallerInfo();
2349
2648
  if (!middleware.metadata) {
2350
2649
  middleware.metadata = {
@@ -2355,32 +2654,36 @@ class Shokupan extends ShokupanRouter {
2355
2654
  pluginName: middleware.pluginName
2356
2655
  };
2357
2656
  }
2358
- trackedMiddleware = async (ctx, next) => {
2359
- const c = ctx;
2360
- if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2361
- const metadata = middleware.metadata || {};
2362
- const start = performance.now();
2363
- const item = {
2364
- name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2365
- file: metadata.file || file,
2366
- line: metadata.line || line,
2367
- isBuiltin: metadata.isBuiltin,
2368
- startTime: start,
2369
- duration: -1
2370
- };
2371
- c.handlerStack.push(item);
2372
- try {
2373
- return await middleware(ctx, next);
2374
- } finally {
2375
- item.duration = performance.now() - start;
2657
+ if (this.applicationConfig.enableMiddlewareTracking) {
2658
+ const trackedMiddleware = async (ctx, next) => {
2659
+ const c = ctx;
2660
+ if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2661
+ const metadata = middleware.metadata || {};
2662
+ const start = performance.now();
2663
+ const item = {
2664
+ name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2665
+ file: metadata.file || file,
2666
+ line: metadata.line || line,
2667
+ isBuiltin: metadata.isBuiltin,
2668
+ startTime: start,
2669
+ duration: -1
2670
+ };
2671
+ c.handlerStack.push(item);
2672
+ try {
2673
+ return await middleware(ctx, next);
2674
+ } finally {
2675
+ item.duration = performance.now() - start;
2676
+ }
2376
2677
  }
2377
- }
2378
- return middleware(ctx, next);
2379
- };
2380
- trackedMiddleware.metadata = middleware.metadata;
2381
- Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2382
- trackedMiddleware.order = this.middleware.length;
2383
- this.middleware.push(trackedMiddleware);
2678
+ return middleware(ctx, next);
2679
+ };
2680
+ trackedMiddleware.metadata = middleware.metadata;
2681
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2682
+ trackedMiddleware.order = this.middleware.length;
2683
+ this.middleware.push(trackedMiddleware);
2684
+ } else {
2685
+ this.middleware.push(middleware);
2686
+ }
2384
2687
  return this;
2385
2688
  }
2386
2689
  startupHooks = [];
@@ -2411,17 +2714,13 @@ class Shokupan extends ShokupanRouter {
2411
2714
  if (finalPort < 0 || finalPort > 65535) {
2412
2715
  throw new Error("Invalid port number");
2413
2716
  }
2414
- for (const hook of this.startupHooks) {
2415
- await hook();
2416
- }
2717
+ await Promise.all(this.startupHooks.map((hook) => hook()));
2417
2718
  if (this.applicationConfig.enableOpenApiGen) {
2418
2719
  this.openApiSpec = await generateOpenApi(this);
2419
- for (const hook of this.specAvailableHooks) {
2420
- await hook(this.openApiSpec);
2421
- }
2720
+ await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
2422
2721
  }
2423
2722
  if (port === 0 && process.platform === "linux") ;
2424
- if (this.applicationConfig.autoBackpressureFeedback) {
2723
+ if (this.applicationConfig.autoBackpressureFeedback === true) {
2425
2724
  this.cpuMonitor = new SystemCpuMonitor();
2426
2725
  this.cpuMonitor.start();
2427
2726
  }
@@ -2449,7 +2748,7 @@ class Shokupan extends ShokupanRouter {
2449
2748
  };
2450
2749
  let factory = this.applicationConfig.serverFactory;
2451
2750
  if (!factory && typeof Bun === "undefined") {
2452
- const { createHttpServer } = await import("./server-adapter-BWrEJbKL.js");
2751
+ const { createHttpServer } = await import("./server-adapter-0xH174zz.js");
2453
2752
  factory = createHttpServer();
2454
2753
  }
2455
2754
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2462,7 +2761,7 @@ class Shokupan extends ShokupanRouter {
2462
2761
  /**
2463
2762
  * Processes a request by wrapping the standard fetch method.
2464
2763
  */
2465
- async processRequest(options) {
2764
+ async testRequest(options) {
2466
2765
  let url = options.url || options.path || "/";
2467
2766
  if (!url.startsWith("http")) {
2468
2767
  const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
@@ -2471,7 +2770,9 @@ class Shokupan extends ShokupanRouter {
2471
2770
  }
2472
2771
  if (options.query) {
2473
2772
  const u = new URL(url);
2474
- for (const [k, v] of Object.entries(options.query)) {
2773
+ const entries = Object.entries(options.query);
2774
+ for (let i = 0; i < entries.length; i++) {
2775
+ const [k, v] = entries[i];
2475
2776
  u.searchParams.set(k, v);
2476
2777
  }
2477
2778
  url = u.toString();
@@ -2540,18 +2841,18 @@ class Shokupan extends ShokupanRouter {
2540
2841
  if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2541
2842
  const msg = "Too Many Requests (CPU Backpressure)";
2542
2843
  const res = ctx.text(msg, 429);
2543
- await this.executeHook("onResponseEnd", ctx, res);
2844
+ await this.runHooks("onResponseEnd", ctx, res);
2544
2845
  return res;
2545
2846
  }
2546
2847
  try {
2547
- if (this.hasHook("onRequestStart")) {
2548
- await this.executeHook("onRequestStart", ctx);
2549
- }
2848
+ await this.runHooks("onRequestStart", ctx);
2550
2849
  const fn = this.composedMiddleware ??= compose(this.middleware);
2551
2850
  const result = await fn(ctx, async () => {
2851
+ const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2552
2852
  const match = this.find(req.method, ctx.path);
2553
2853
  if (match) {
2554
2854
  ctx.params = match.params;
2855
+ await bodyParsing;
2555
2856
  return match.handler(ctx);
2556
2857
  }
2557
2858
  return null;
@@ -2574,12 +2875,8 @@ class Shokupan extends ShokupanRouter {
2574
2875
  } else {
2575
2876
  response = ctx.text(String(result));
2576
2877
  }
2577
- if (this.hasHook("onRequestEnd")) {
2578
- await this.executeHook("onRequestEnd", ctx);
2579
- }
2580
- if (this.hasHook("onResponseStart")) {
2581
- await this.executeHook("onResponseStart", ctx, response);
2582
- }
2878
+ await this.runHooks("onRequestEnd", ctx);
2879
+ await this.runHooks("onResponseStart", ctx, response);
2583
2880
  return response;
2584
2881
  } catch (err) {
2585
2882
  console.error(err);
@@ -2588,9 +2885,7 @@ class Shokupan extends ShokupanRouter {
2588
2885
  const status = err.status || err.statusCode || 500;
2589
2886
  const body = { error: err.message || "Internal Server Error" };
2590
2887
  if (err.errors) body.errors = err.errors;
2591
- if (this.hasHook("onError")) {
2592
- await this.executeHook("onError", err, ctx);
2593
- }
2888
+ await this.runHooks("onError", ctx, err);
2594
2889
  return ctx.json(body, status);
2595
2890
  }
2596
2891
  };
@@ -2601,9 +2896,7 @@ class Shokupan extends ShokupanRouter {
2601
2896
  const timeoutPromise = new Promise((_, reject) => {
2602
2897
  timeoutId = setTimeout(async () => {
2603
2898
  controller.abort();
2604
- if (this.hasHook("onRequestTimeout")) {
2605
- await this.executeHook("onRequestTimeout", ctx);
2606
- }
2899
+ await this.runHooks("onRequestTimeout", ctx);
2607
2900
  reject(new Error("Request Timeout"));
2608
2901
  }, timeoutMs);
2609
2902
  });
@@ -2616,56 +2909,10 @@ class Shokupan extends ShokupanRouter {
2616
2909
  console.error("Unexpected error in request execution:", err);
2617
2910
  return ctx.text("Internal Server Error", 500);
2618
2911
  }).then(async (res) => {
2619
- if (this.hasHook("onResponseEnd")) {
2620
- await this.executeHook("onResponseEnd", ctx, res);
2621
- }
2912
+ await this.runHooks("onResponseEnd", ctx, res);
2622
2913
  return res;
2623
2914
  });
2624
2915
  }
2625
- ensureHooksInitialized() {
2626
- const hooks = this.applicationConfig.hooks;
2627
- if (hooks) {
2628
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
2629
- const hookTypes = [
2630
- "onRequestStart",
2631
- "onRequestEnd",
2632
- "onResponseStart",
2633
- "onResponseEnd",
2634
- "onError",
2635
- "beforeValidate",
2636
- "afterValidate",
2637
- "onRequestTimeout",
2638
- "onReadTimeout",
2639
- "onWriteTimeout"
2640
- ];
2641
- for (const type of hookTypes) {
2642
- const fns = [];
2643
- for (const h of hookList) {
2644
- if (h[type]) fns.push(h[type]);
2645
- }
2646
- if (fns.length > 0) {
2647
- this.hookCache.set(type, fns);
2648
- }
2649
- }
2650
- }
2651
- this.hooksInitialized = true;
2652
- }
2653
- async executeHook(name, ...args) {
2654
- if (!this.hooksInitialized) {
2655
- this.ensureHooksInitialized();
2656
- }
2657
- const fns = this.hookCache.get(name);
2658
- if (!fns) return;
2659
- for (const fn of fns) {
2660
- await fn(...args);
2661
- }
2662
- }
2663
- hasHook(name) {
2664
- if (!this.hooksInitialized) {
2665
- this.ensureHooksInitialized();
2666
- }
2667
- return this.hookCache.has(name);
2668
- }
2669
2916
  }
2670
2917
  class AuthPlugin extends ShokupanRouter {
2671
2918
  constructor(authConfig) {
@@ -2713,7 +2960,9 @@ class AuthPlugin extends ShokupanRouter {
2713
2960
  return jwt;
2714
2961
  }
2715
2962
  init() {
2716
- for (const [providerName, providerConfig] of Object.entries(this.authConfig.providers)) {
2963
+ const providerEntries = Object.entries(this.authConfig.providers);
2964
+ for (let i = 0; i < providerEntries.length; i++) {
2965
+ const [providerName, providerConfig] = providerEntries[i];
2717
2966
  if (!providerConfig) continue;
2718
2967
  const provider = this.getProviderInstance(providerName, providerConfig);
2719
2968
  if (!provider) {
@@ -2736,9 +2985,10 @@ class AuthPlugin extends ShokupanRouter {
2736
2985
  } else {
2737
2986
  return ctx.text("Provider config error", 500);
2738
2987
  }
2739
- ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; Max-Age=600`);
2988
+ const isSecure = ctx.secure;
2989
+ ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
2740
2990
  if (codeVerifier) {
2741
- ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; Max-Age=600`);
2991
+ ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
2742
2992
  }
2743
2993
  return ctx.redirect(url.toString());
2744
2994
  });
@@ -2779,7 +3029,7 @@ class AuthPlugin extends ShokupanRouter {
2779
3029
  return ctx.json({ token: jwt, user });
2780
3030
  } catch (e) {
2781
3031
  console.error("Auth Error", e);
2782
- return ctx.text("Authentication failed: " + e.message + "\n" + e.stack, 500);
3032
+ return ctx.text("Authentication failed. Please try again.", 500);
2783
3033
  }
2784
3034
  });
2785
3035
  }
@@ -2990,14 +3240,21 @@ function Cors(options = {}) {
2990
3240
  const origin = ctx.headers.get("origin");
2991
3241
  const set = (k, v) => headers.set(k, v);
2992
3242
  const append = (k, v) => headers.append(k, v);
3243
+ if (origin === "null" && opts.origin !== "null") {
3244
+ return next();
3245
+ }
2993
3246
  if (opts.origin === "*") {
2994
3247
  set("Access-Control-Allow-Origin", "*");
2995
3248
  } else if (typeof opts.origin === "string") {
2996
3249
  set("Access-Control-Allow-Origin", opts.origin);
2997
3250
  } else if (Array.isArray(opts.origin)) {
2998
- if (origin && opts.origin.includes(origin)) {
2999
- set("Access-Control-Allow-Origin", origin);
3000
- append("Vary", "Origin");
3251
+ if (origin) {
3252
+ const normalizedOrigin = origin.toLowerCase();
3253
+ const normalizedAllowed = opts.origin.map((o) => o.toLowerCase());
3254
+ if (normalizedAllowed.includes(normalizedOrigin)) {
3255
+ set("Access-Control-Allow-Origin", origin);
3256
+ append("Vary", "Origin");
3257
+ }
3001
3258
  }
3002
3259
  } else if (typeof opts.origin === "function") {
3003
3260
  const allowed = opts.origin(ctx);
@@ -3041,7 +3298,9 @@ function Cors(options = {}) {
3041
3298
  }
3042
3299
  const response = await next();
3043
3300
  if (response instanceof Response) {
3044
- for (const [key, value] of headers.entries()) {
3301
+ const headerEntries = Array.from(headers.entries());
3302
+ for (let i = 0; i < headerEntries.length; i++) {
3303
+ const [key, value] = headerEntries[i];
3045
3304
  response.headers.set(key, value);
3046
3305
  }
3047
3306
  }
@@ -3111,6 +3370,8 @@ function useExpress(expressMiddleware) {
3111
3370
  });
3112
3371
  };
3113
3372
  }
3373
+ let plainToInstance;
3374
+ let validateOrReject;
3114
3375
  class ValidationError extends Error {
3115
3376
  constructor(errors) {
3116
3377
  super("Validation Error");
@@ -3175,6 +3436,18 @@ function isClass(schema) {
3175
3436
  }
3176
3437
  }
3177
3438
  async function validateClassValidator(schema, data) {
3439
+ if (!plainToInstance || !validateOrReject) {
3440
+ try {
3441
+ const ct = await import("class-transformer");
3442
+ const cv = await import("class-validator");
3443
+ plainToInstance = ct.plainToInstance;
3444
+ validateOrReject = cv.validateOrReject;
3445
+ } catch (e) {
3446
+ throw new Error(
3447
+ "class-transformer and class-validator are required for class-based validation. Install them with: bun add class-transformer class-validator reflect-metadata"
3448
+ );
3449
+ }
3450
+ }
3178
3451
  const object = plainToInstance(schema, data);
3179
3452
  try {
3180
3453
  await validateOrReject(object);
@@ -3189,30 +3462,8 @@ async function validateClassValidator(schema, data) {
3189
3462
  }
3190
3463
  }
3191
3464
  const safelyGetBody = async (ctx) => {
3192
- const req = ctx.req;
3193
- if (req._bodyParsed) {
3194
- return req._bodyValue;
3195
- }
3196
3465
  try {
3197
- let data;
3198
- if (typeof req.json === "function") {
3199
- data = await req.json();
3200
- } else {
3201
- data = req.body;
3202
- if (typeof data === "string") {
3203
- try {
3204
- data = JSON.parse(data);
3205
- } catch {
3206
- }
3207
- }
3208
- }
3209
- req._bodyParsed = true;
3210
- req._bodyValue = data;
3211
- Object.defineProperty(req, "json", {
3212
- value: async () => req._bodyValue,
3213
- configurable: true
3214
- });
3215
- return data;
3466
+ return await ctx.body();
3216
3467
  } catch (e) {
3217
3468
  return {};
3218
3469
  }
@@ -3259,9 +3510,7 @@ function validate(config) {
3259
3510
  body = await safelyGetBody(ctx);
3260
3511
  dataToValidate.body = body;
3261
3512
  }
3262
- if (ctx.app?.hasHook("beforeValidate")) {
3263
- await ctx.app.executeHook("beforeValidate", ctx, dataToValidate);
3264
- }
3513
+ await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
3265
3514
  if (validators.params) {
3266
3515
  ctx.params = await validators.params(ctx.params);
3267
3516
  }
@@ -3277,21 +3526,20 @@ function validate(config) {
3277
3526
  if (validators.body) {
3278
3527
  const b = body ?? await safelyGetBody(ctx);
3279
3528
  validBody = await validators.body(b);
3529
+ ctx._cachedBody = validBody;
3280
3530
  const req = ctx.req;
3281
- req._bodyValue = validBody;
3282
3531
  Object.defineProperty(req, "json", {
3283
3532
  value: async () => validBody,
3533
+ writable: true,
3284
3534
  configurable: true
3285
3535
  });
3286
3536
  ctx.body = validBody;
3287
3537
  }
3288
- if (ctx.app?.hasHook("afterValidate")) {
3289
- const validatedData = { ...dataToValidate };
3290
- if (config.params) validatedData.params = ctx.params;
3291
- if (config.query) validatedData.query = validQuery;
3292
- if (config.body) validatedData.body = validBody;
3293
- await ctx.app?.executeHook("afterValidate", ctx, validatedData);
3294
- }
3538
+ const validatedData = { ...dataToValidate };
3539
+ if (config.params) validatedData.params = ctx.params;
3540
+ if (config.query) validatedData.query = validQuery;
3541
+ if (config.body) validatedData.body = validBody;
3542
+ await ctx.app.runHooks("afterValidate", ctx, validatedData);
3295
3543
  return next();
3296
3544
  };
3297
3545
  }
@@ -3314,12 +3562,14 @@ function openApiValidator() {
3314
3562
  if (cache.validators.has(ctx.path)) {
3315
3563
  matchPath = ctx.path;
3316
3564
  } else {
3317
- for (const [path, { regex, paramNames }] of cache.paths) {
3565
+ const pathEntries = Array.from(cache.paths.entries());
3566
+ for (let i = 0; i < pathEntries.length; i++) {
3567
+ const [path, { regex, paramNames }] = pathEntries[i];
3318
3568
  const match = regex.exec(ctx.path);
3319
3569
  if (match) {
3320
3570
  matchPath = path;
3321
- paramNames.forEach((name, i) => {
3322
- matchParams[name] = match[i + 1];
3571
+ paramNames.forEach((name, i2) => {
3572
+ matchParams[name] = match[i2 + 1];
3323
3573
  });
3324
3574
  break;
3325
3575
  }
@@ -3376,7 +3626,9 @@ function openApiValidator() {
3376
3626
  function compileValidators(spec) {
3377
3627
  const validators = /* @__PURE__ */ new Map();
3378
3628
  const paths = /* @__PURE__ */ new Map();
3379
- for (const [path, pathItem] of Object.entries(spec.paths || {})) {
3629
+ const pathEntries = Object.entries(spec.paths || {});
3630
+ for (let i = 0; i < pathEntries.length; i++) {
3631
+ const [path, pathItem] = pathEntries[i];
3380
3632
  if (path.includes("{")) {
3381
3633
  const paramNames = [];
3382
3634
  const regexStr = "^" + path.replace(/{([^}]+)}/g, (_, name) => {
@@ -3389,7 +3641,9 @@ function compileValidators(spec) {
3389
3641
  });
3390
3642
  }
3391
3643
  const pathValidators = {};
3392
- for (const [method, operation] of Object.entries(pathItem)) {
3644
+ const methodEntries = Object.entries(pathItem);
3645
+ for (let k = 0; k < methodEntries.length; k++) {
3646
+ const [method, operation] = methodEntries[k];
3393
3647
  if (method === "parameters" || method === "summary" || method === "description") continue;
3394
3648
  const oper = operation;
3395
3649
  const opValidators = {};
@@ -3403,7 +3657,8 @@ function compileValidators(spec) {
3403
3657
  const queryRequired = [];
3404
3658
  const pathRequired = [];
3405
3659
  const headerRequired = [];
3406
- for (const param of parameters) {
3660
+ for (let j = 0; j < parameters.length; j++) {
3661
+ const param = parameters[j];
3407
3662
  if (param.in === "query") {
3408
3663
  queryProps[param.name] = param.schema || {};
3409
3664
  if (param.required) queryRequired.push(param.name);
@@ -3564,14 +3819,18 @@ function SecurityHeaders(options = {}) {
3564
3819
  if (opt === void 0 || opt === true) {
3565
3820
  set("Content-Security-Policy", "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests");
3566
3821
  } else if (typeof opt === "object") {
3567
- for (const [key, val] of Object.entries(opt)) {
3822
+ const optEntries = Object.entries(opt);
3823
+ for (let i = 0; i < optEntries.length; i++) {
3824
+ const [key, val] = optEntries[i];
3568
3825
  }
3569
3826
  }
3570
3827
  }
3571
3828
  if (options.hidePoweredBy !== false) ;
3572
3829
  const response = await next();
3573
3830
  if (response instanceof Response) {
3574
- for (const [k, v] of Object.entries(headers)) {
3831
+ const headerEntries = Object.entries(headers);
3832
+ for (let i = 0; i < headerEntries.length; i++) {
3833
+ const [k, v] = headerEntries[i];
3575
3834
  response.headers.set(k, v);
3576
3835
  }
3577
3836
  return response;
@@ -3657,7 +3916,9 @@ class MemoryStore extends EventEmitter {
3657
3916
  }
3658
3917
  all(cb) {
3659
3918
  const result = {};
3660
- for (const sid in this.sessions) {
3919
+ const sessionKeys = Object.keys(this.sessions);
3920
+ for (let i = 0; i < sessionKeys.length; i++) {
3921
+ const sid = sessionKeys[i];
3661
3922
  try {
3662
3923
  result[sid] = JSON.parse(this.sessions[sid]);
3663
3924
  } catch {
@@ -3680,11 +3941,17 @@ function unsign(input, secret) {
3680
3941
  if (typeof secret !== "string") throw new TypeError("Secret string must be provided.");
3681
3942
  const tentValue = input.slice(0, input.lastIndexOf("."));
3682
3943
  const expectedInput = sign(tentValue, secret);
3683
- const expectedBuffer = Buffer.from(expectedInput);
3684
- const inputBuffer = Buffer.from(input);
3685
- if (expectedBuffer.length !== inputBuffer.length) return false;
3686
- const valid = require("crypto").timingSafeEqual(expectedBuffer, inputBuffer);
3687
- return valid ? tentValue : false;
3944
+ const maxLength = Math.max(expectedInput.length, input.length);
3945
+ const paddedExpected = Buffer.alloc(maxLength);
3946
+ const paddedInput = Buffer.alloc(maxLength);
3947
+ Buffer.from(expectedInput).copy(paddedExpected);
3948
+ Buffer.from(input).copy(paddedInput);
3949
+ try {
3950
+ const valid = require("crypto").timingSafeEqual(paddedExpected, paddedInput);
3951
+ return valid ? tentValue : false;
3952
+ } catch {
3953
+ return false;
3954
+ }
3688
3955
  }
3689
3956
  function Session(options) {
3690
3957
  const store = options.store || new MemoryStore();
@@ -3743,7 +4010,9 @@ function Session(options) {
3743
4010
  sessObj.regenerate = (cb) => {
3744
4011
  store.destroy(sessObj.id, (err) => {
3745
4012
  sessionID = generateId(ctx);
3746
- for (const key in sessObj) {
4013
+ const keys = Object.keys(sessObj);
4014
+ for (let i = 0; i < keys.length; i++) {
4015
+ const key = keys[i];
3747
4016
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3748
4017
  delete sessObj[key];
3749
4018
  }
@@ -3758,7 +4027,9 @@ function Session(options) {
3758
4027
  store.get(sessObj.id, (err, sess2) => {
3759
4028
  if (err) return cb(err);
3760
4029
  if (!sess2) return cb(new Error("Session not found"));
3761
- for (const key in sessObj) {
4030
+ const keys = Object.keys(sessObj);
4031
+ for (let i = 0; i < keys.length; i++) {
4032
+ const key = keys[i];
3762
4033
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3763
4034
  delete sessObj[key];
3764
4035
  }