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.cjs CHANGED
@@ -1,12 +1,32 @@
1
1
  "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
2
24
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
25
  const promises = require("node:fs/promises");
4
26
  const eta$2 = require("eta");
5
27
  const promises$1 = require("fs/promises");
6
28
  const path = require("path");
7
29
  const node_async_hooks = require("node:async_hooks");
8
- const node = require("@surrealdb/node");
9
- const surrealdb = require("surrealdb");
10
30
  const api = require("@opentelemetry/api");
11
31
  const os = require("node:os");
12
32
  const arctic = require("arctic");
@@ -14,9 +34,7 @@ const jose = require("jose");
14
34
  const zlib = require("node:zlib");
15
35
  const Ajv = require("ajv");
16
36
  const addFormats = require("ajv-formats");
17
- const classTransformer = require("class-transformer");
18
- const classValidator = require("class-validator");
19
- const openapiAnalyzer = require("./openapi-analyzer-z-7AoFRC.cjs");
37
+ const openapiAnalyzer = require("./openapi-analyzer-Bei1sVWp.cjs");
20
38
  const crypto = require("crypto");
21
39
  const events = require("events");
22
40
  function _interopNamespaceDefault(e) {
@@ -101,8 +119,82 @@ class ShokupanResponse {
101
119
  return this._headers !== null;
102
120
  }
103
121
  }
122
+ function isValidCookieDomain(domain, currentHost) {
123
+ const hostWithoutPort = currentHost.split(":")[0];
124
+ if (domain === hostWithoutPort) return true;
125
+ if (domain.startsWith(".")) {
126
+ const domainWithoutDot = domain.slice(1);
127
+ return hostWithoutPort.endsWith(domainWithoutDot);
128
+ }
129
+ return false;
130
+ }
131
+ const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
132
+ 100,
133
+ 101,
134
+ 102,
135
+ 103,
136
+ 200,
137
+ 201,
138
+ 202,
139
+ 203,
140
+ 204,
141
+ 205,
142
+ 206,
143
+ 207,
144
+ 208,
145
+ 226,
146
+ 300,
147
+ 301,
148
+ 302,
149
+ 303,
150
+ 304,
151
+ 305,
152
+ 306,
153
+ 307,
154
+ 308,
155
+ 400,
156
+ 401,
157
+ 402,
158
+ 403,
159
+ 404,
160
+ 405,
161
+ 406,
162
+ 407,
163
+ 408,
164
+ 409,
165
+ 410,
166
+ 411,
167
+ 412,
168
+ 413,
169
+ 414,
170
+ 415,
171
+ 416,
172
+ 417,
173
+ 418,
174
+ 421,
175
+ 422,
176
+ 423,
177
+ 424,
178
+ 425,
179
+ 426,
180
+ 428,
181
+ 429,
182
+ 431,
183
+ 451,
184
+ 500,
185
+ 501,
186
+ 502,
187
+ 503,
188
+ 504,
189
+ 505,
190
+ 506,
191
+ 507,
192
+ 508,
193
+ 510,
194
+ 511
195
+ ]);
196
+ const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
104
197
  class ShokupanContext {
105
- // Raw body for compression optimization
106
198
  constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
107
199
  this.request = request;
108
200
  this.server = server;
@@ -125,7 +217,6 @@ class ShokupanContext {
125
217
  }
126
218
  this.response = new ShokupanResponse();
127
219
  }
128
- _url;
129
220
  params = {};
130
221
  // Router assigns this, but default to empty object
131
222
  state;
@@ -134,6 +225,19 @@ class ShokupanContext {
134
225
  _debug;
135
226
  _finalResponse;
136
227
  _rawBody;
228
+ // Raw body for compression optimization
229
+ // Body caching to avoid double parsing
230
+ _url;
231
+ _cachedBody;
232
+ _bodyType;
233
+ _bodyParsed = false;
234
+ _bodyParseError;
235
+ // Cached URL properties to avoid repeated parsing
236
+ _cachedHostname;
237
+ _cachedProtocol;
238
+ _cachedHost;
239
+ _cachedOrigin;
240
+ _cachedQuery;
137
241
  get url() {
138
242
  if (!this._url) {
139
243
  const urlString = this.request.url || "http://localhost/";
@@ -182,16 +286,24 @@ class ShokupanContext {
182
286
  * Request query params
183
287
  */
184
288
  get query() {
185
- const q = {};
186
- for (const [key, value] of this.url.searchParams) {
187
- if (q[key] === void 0) {
188
- q[key] = value;
189
- } else if (Array.isArray(q[key])) {
190
- q[key].push(value);
289
+ if (this._cachedQuery) return this._cachedQuery;
290
+ const q = /* @__PURE__ */ Object.create(null);
291
+ const blocklist = ["__proto__", "constructor", "prototype"];
292
+ const entries = Object.entries(this.url.searchParams);
293
+ for (let i = 0; i < entries.length; i++) {
294
+ const [key, value] = entries[i];
295
+ if (blocklist.includes(key)) continue;
296
+ if (Object.prototype.hasOwnProperty.call(q, key)) {
297
+ if (Array.isArray(q[key])) {
298
+ q[key].push(value);
299
+ } else {
300
+ q[key] = [q[key], value];
301
+ }
191
302
  } else {
192
- q[key] = [q[key], value];
303
+ q[key] = value;
193
304
  }
194
305
  }
306
+ this._cachedQuery = q;
195
307
  return q;
196
308
  }
197
309
  /**
@@ -204,31 +316,31 @@ class ShokupanContext {
204
316
  * Request hostname (e.g. "localhost")
205
317
  */
206
318
  get hostname() {
207
- return this.url.hostname;
319
+ return this._cachedHostname ??= this.url.hostname;
208
320
  }
209
321
  /**
210
322
  * Request host (e.g. "localhost:3000")
211
323
  */
212
324
  get host() {
213
- return this.url.host;
325
+ return this._cachedHost ??= this.url.host;
214
326
  }
215
327
  /**
216
328
  * Request protocol (e.g. "http:", "https:")
217
329
  */
218
330
  get protocol() {
219
- return this.url.protocol;
331
+ return this._cachedProtocol ??= this.url.protocol;
220
332
  }
221
333
  /**
222
334
  * Whether request is secure (https)
223
335
  */
224
336
  get secure() {
225
- return this.url.protocol === "https:";
337
+ return this.protocol === "https:";
226
338
  }
227
339
  /**
228
340
  * Request origin (e.g. "http://localhost:3000")
229
341
  */
230
342
  get origin() {
231
- return this.url.origin;
343
+ return this._cachedOrigin ??= this.url.origin;
232
344
  }
233
345
  /**
234
346
  * Request headers
@@ -265,6 +377,12 @@ class ShokupanContext {
265
377
  * @param options Cookie options
266
378
  */
267
379
  setCookie(name, value, options = {}) {
380
+ if (options.domain) {
381
+ const currentHost = this.hostname;
382
+ if (!isValidCookieDomain(options.domain, currentHost)) {
383
+ throw new Error(`Invalid cookie domain: ${options.domain} for host ${currentHost}`);
384
+ }
385
+ }
268
386
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
269
387
  if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
270
388
  if (options.domain) cookie += `; Domain=${options.domain}`;
@@ -324,6 +442,91 @@ class ShokupanContext {
324
442
  }
325
443
  return h;
326
444
  }
445
+ /**
446
+ * Read request body with caching to avoid double parsing.
447
+ * The body is only parsed once and cached for subsequent reads.
448
+ */
449
+ async body() {
450
+ if (this._bodyParseError) {
451
+ throw this._bodyParseError;
452
+ }
453
+ if (this._bodyParsed) {
454
+ return this._cachedBody;
455
+ }
456
+ const contentType = this.request.headers.get("content-type") || "";
457
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
458
+ const rawText = await this.readRawBody();
459
+ const parserType = this.app?.applicationConfig?.jsonParser || "native";
460
+ if (parserType === "native") {
461
+ this._cachedBody = JSON.parse(rawText);
462
+ } else {
463
+ const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
464
+ const parser = getJSONParser(parserType);
465
+ this._cachedBody = parser(rawText);
466
+ }
467
+ this._bodyType = "json";
468
+ } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
469
+ this._cachedBody = await this.request.formData();
470
+ this._bodyType = "formData";
471
+ } else {
472
+ this._cachedBody = await this.readRawBody();
473
+ this._bodyType = "text";
474
+ }
475
+ this._bodyParsed = true;
476
+ return this._cachedBody;
477
+ }
478
+ /**
479
+ * Pre-parse the request body before handler execution.
480
+ * This improves performance and enables Node.js compatibility for large payloads.
481
+ * Errors are deferred until the body is actually accessed in the handler.
482
+ */
483
+ async parseBody() {
484
+ if (this._bodyParsed) {
485
+ return;
486
+ }
487
+ if (this.request.method === "GET" || this.request.method === "HEAD") {
488
+ return;
489
+ }
490
+ try {
491
+ await this.body();
492
+ } catch (error) {
493
+ this._bodyParseError = error;
494
+ }
495
+ }
496
+ /**
497
+ * Read raw body from ReadableStream efficiently.
498
+ * This is much faster than request.text() for large payloads.
499
+ * Also handles the case where body is already a string (e.g., in tests).
500
+ */
501
+ async readRawBody() {
502
+ if (typeof this.request.body === "string") {
503
+ return this.request.body;
504
+ }
505
+ const reader = this.request.body?.getReader();
506
+ if (!reader) {
507
+ return "";
508
+ }
509
+ const chunks = [];
510
+ let totalSize = 0;
511
+ try {
512
+ while (true) {
513
+ const { done, value } = await reader.read();
514
+ if (done) break;
515
+ chunks.push(value);
516
+ totalSize += value.length;
517
+ }
518
+ } finally {
519
+ reader.releaseLock();
520
+ }
521
+ const result = new Uint8Array(totalSize);
522
+ let offset = 0;
523
+ for (let i = 0; i < chunks.length; i++) {
524
+ const chunk = chunks[i];
525
+ result.set(chunk, offset);
526
+ offset += chunk.length;
527
+ }
528
+ return new TextDecoder().decode(result);
529
+ }
327
530
  /**
328
531
  * Send a response
329
532
  * @param body Response body
@@ -332,31 +535,24 @@ class ShokupanContext {
332
535
  */
333
536
  send(body, options) {
334
537
  const headers = this.mergeHeaders(options?.headers);
335
- const status = options?.status ?? this.response.status;
538
+ const status = options?.status ?? this.response.status ?? 200;
539
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
540
+ throw new Error(`Invalid HTTP status code: ${status}`);
541
+ }
336
542
  if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
337
543
  this._rawBody = body;
338
544
  }
339
545
  this._finalResponse = new Response(body, { status, headers });
340
546
  return this._finalResponse;
341
547
  }
342
- /**
343
- * Read request body
344
- */
345
- async body() {
346
- const contentType = this.request.headers.get("content-type") || "";
347
- if (contentType.includes("application/json") || contentType.includes("+json")) {
348
- return this.request.json();
349
- }
350
- if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
351
- return this.request.formData();
352
- }
353
- return this.request.text();
354
- }
355
548
  /**
356
549
  * Respond with a JSON object
357
550
  */
358
551
  json(data, status, headers) {
359
- const finalStatus = status ?? this.response.status;
552
+ const finalStatus = status ?? this.response.status ?? 200;
553
+ if (!VALID_HTTP_STATUSES.has(finalStatus)) {
554
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
555
+ }
360
556
  const jsonString = JSON.stringify(data);
361
557
  this._rawBody = jsonString;
362
558
  if (!headers && !this.response.hasPopulatedHeaders) {
@@ -375,7 +571,10 @@ class ShokupanContext {
375
571
  * Respond with a text string
376
572
  */
377
573
  text(data, status, headers) {
378
- const finalStatus = status ?? this.response.status;
574
+ const finalStatus = status ?? this.response.status ?? 200;
575
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
576
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
577
+ }
379
578
  this._rawBody = data;
380
579
  if (!headers && !this.response.hasPopulatedHeaders) {
381
580
  this._finalResponse = new Response(data, {
@@ -393,7 +592,10 @@ class ShokupanContext {
393
592
  * Respond with HTML content
394
593
  */
395
594
  html(html, status, headers) {
396
- const finalStatus = status ?? this.response.status;
595
+ const finalStatus = status ?? this.response.status ?? 200;
596
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
597
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
598
+ }
397
599
  const finalHeaders = this.mergeHeaders(headers);
398
600
  finalHeaders.set("content-type", "text/html; charset=utf-8");
399
601
  this._rawBody = html;
@@ -404,6 +606,9 @@ class ShokupanContext {
404
606
  * Respond with a redirect
405
607
  */
406
608
  redirect(url, status = 302) {
609
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
610
+ throw new Error(`Invalid redirect status code: ${status}`);
611
+ }
407
612
  const headers = this.mergeHeaders();
408
613
  headers.set("Location", url);
409
614
  this._finalResponse = new Response(null, { status, headers });
@@ -414,6 +619,9 @@ class ShokupanContext {
414
619
  * DOES NOT CHAIN!
415
620
  */
416
621
  status(status) {
622
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
623
+ throw new Error(`Invalid HTTP status code: ${status}`);
624
+ }
417
625
  const headers = this.mergeHeaders();
418
626
  this._finalResponse = new Response(null, { status, headers });
419
627
  return this._finalResponse;
@@ -424,6 +632,9 @@ class ShokupanContext {
424
632
  async file(path2, fileOptions, responseOptions) {
425
633
  const headers = this.mergeHeaders(responseOptions?.headers);
426
634
  const status = responseOptions?.status ?? this.response.status;
635
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
636
+ throw new Error(`Invalid HTTP status code: ${status}`);
637
+ }
427
638
  if (typeof Bun !== "undefined") {
428
639
  this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
429
640
  return this._finalResponse;
@@ -447,6 +658,10 @@ class ShokupanContext {
447
658
  * @param headers HTTP Headers
448
659
  */
449
660
  async jsx(element, args, status, headers) {
661
+ status ??= 200;
662
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
663
+ throw new Error(`Invalid HTTP status code: ${status}`);
664
+ }
450
665
  if (!this.renderer) {
451
666
  throw new Error("No JSX renderer configured");
452
667
  }
@@ -461,17 +676,32 @@ function RateLimitMiddleware(options = {}) {
461
676
  const statusCode = options.statusCode || 429;
462
677
  const headers = options.headers !== false;
463
678
  const mode = options.mode || "user";
679
+ const trustedProxies = options.trustedProxies || [];
464
680
  const keyGenerator = options.keyGenerator || ((ctx) => {
465
681
  if (mode === "absolute") {
466
682
  return "global";
467
683
  }
468
- return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
684
+ const xForwardedFor = ctx.headers.get("x-forwarded-for");
685
+ if (xForwardedFor && trustedProxies.length > 0) {
686
+ const ips = xForwardedFor.split(",").map((ip) => ip.trim());
687
+ for (let i = ips.length - 1; i >= 0; i--) {
688
+ const ip = ips[i];
689
+ if (!trustedProxies.includes(ip)) {
690
+ if (/^[\d.:a-fA-F]+$/.test(ip)) {
691
+ return ip;
692
+ }
693
+ }
694
+ }
695
+ }
696
+ return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
469
697
  });
470
698
  const skip = options.skip || (() => false);
471
699
  const hits = /* @__PURE__ */ new Map();
472
700
  const interval = setInterval(() => {
473
701
  const now = Date.now();
474
- for (const [key, record] of hits.entries()) {
702
+ const entries = Array.from(hits.entries());
703
+ for (let i = 0; i < entries.length; i++) {
704
+ const [key, record] = entries[i];
475
705
  if (record.resetTime <= now) {
476
706
  hits.delete(key);
477
707
  }
@@ -724,7 +954,9 @@ function deepMerge(target, ...sources) {
724
954
  if (!sources.length) return target;
725
955
  const source = sources.shift();
726
956
  if (isObject(target) && isObject(source)) {
727
- for (const key in source) {
957
+ const sourceKeys = Object.keys(source);
958
+ for (let i = 0; i < sourceKeys.length; i++) {
959
+ const key = sourceKeys[i];
728
960
  if (isObject(source[key])) {
729
961
  if (!target[key]) Object.assign(target, { [key]: {} });
730
962
  deepMerge(target[key], source[key]);
@@ -748,15 +980,17 @@ function deepMerge(target, ...sources) {
748
980
  }
749
981
  return deepMerge(target, ...sources);
750
982
  }
751
- const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
752
- const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
753
- const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
754
- const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
755
- const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
756
- const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
757
- const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
758
- const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
759
- const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
983
+ const REGEX_PATTERNS = {
984
+ QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
985
+ QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
986
+ QUERY_NUMBER: /Number\(ctx\.query\.(\w+)\)/g,
987
+ QUERY_BOOL: /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g,
988
+ QUERY_GENERIC: /ctx\.query\.(\w+)/g,
989
+ PARAM_INT: /parseInt\(ctx\.params\.(\w+)\)/g,
990
+ PARAM_FLOAT: /parseFloat\(ctx\.params\.(\w+)\)/g,
991
+ HEADER_GET: /ctx\.get\(['"](\w+)['"]\)/g,
992
+ ERROR_STATUS: /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g
993
+ };
760
994
  function analyzeHandler(handler) {
761
995
  const handlerSource = handler.toString();
762
996
  const inferredSpec = {};
@@ -766,29 +1000,20 @@ function analyzeHandler(handler) {
766
1000
  };
767
1001
  }
768
1002
  const queryParams = /* @__PURE__ */ new Map();
769
- for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
770
- if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
771
- }
772
- for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
773
- if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
774
- }
775
- for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
776
- if (match[1] && !queryParams.has(match[1])) {
777
- queryParams.set(match[1], { type: "number" });
778
- }
779
- }
780
- for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
781
- const name = match[1] || match[2];
782
- if (name && !queryParams.has(name)) {
783
- queryParams.set(name, { type: "boolean" });
784
- }
785
- }
786
- for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
787
- const name = match[1];
788
- if (name && !queryParams.has(name)) {
789
- queryParams.set(name, { type: "string" });
1003
+ const processMatches = (regex, type, format) => {
1004
+ const matches = Array.from(handlerSource.matchAll(regex));
1005
+ for (const match of matches) {
1006
+ const name = match[1] || match[2];
1007
+ if (name && !queryParams.has(name)) {
1008
+ queryParams.set(name, { type, format });
1009
+ }
790
1010
  }
791
- }
1011
+ };
1012
+ processMatches(REGEX_PATTERNS.QUERY_INT, "integer", "int32");
1013
+ processMatches(REGEX_PATTERNS.QUERY_FLOAT, "number", "float");
1014
+ processMatches(REGEX_PATTERNS.QUERY_NUMBER, "number");
1015
+ processMatches(REGEX_PATTERNS.QUERY_BOOL, "boolean");
1016
+ processMatches(REGEX_PATTERNS.QUERY_GENERIC, "string");
792
1017
  if (queryParams.size > 0) {
793
1018
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
794
1019
  queryParams.forEach((schema, paramName) => {
@@ -800,12 +1025,15 @@ function analyzeHandler(handler) {
800
1025
  });
801
1026
  }
802
1027
  const pathParams = /* @__PURE__ */ new Map();
803
- for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
804
- if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
805
- }
806
- for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
807
- if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
808
- }
1028
+ const processPathMatches = (regex, type, format) => {
1029
+ const matches = Array.from(handlerSource.matchAll(regex));
1030
+ for (const match of matches) {
1031
+ const name = match[1];
1032
+ if (name) pathParams.set(name, { type, format });
1033
+ }
1034
+ };
1035
+ processPathMatches(REGEX_PATTERNS.PARAM_INT, "integer", "int32");
1036
+ processPathMatches(REGEX_PATTERNS.PARAM_FLOAT, "number", "float");
809
1037
  if (pathParams.size > 0) {
810
1038
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
811
1039
  pathParams.forEach((schema, paramName) => {
@@ -817,7 +1045,8 @@ function analyzeHandler(handler) {
817
1045
  });
818
1046
  });
819
1047
  }
820
- for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
1048
+ const headerMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.HEADER_GET));
1049
+ for (const match of headerMatches) {
821
1050
  if (match[1]) {
822
1051
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
823
1052
  inferredSpec.parameters.push({
@@ -836,13 +1065,19 @@ function analyzeHandler(handler) {
836
1065
  }
837
1066
  if (handlerSource.includes("ctx.html(")) {
838
1067
  responses["200"] = {
839
- description: "Successful response",
1068
+ description: "Successful HTML response",
1069
+ content: { "text/html": { schema: { type: "string" } } }
1070
+ };
1071
+ }
1072
+ if (handlerSource.includes("ctx.jsx(")) {
1073
+ responses["200"] = {
1074
+ description: "Successful HTML response (Rendered JSX)",
840
1075
  content: { "text/html": { schema: { type: "string" } } }
841
1076
  };
842
1077
  }
843
1078
  if (handlerSource.includes("ctx.text(")) {
844
1079
  responses["200"] = {
845
- description: "Successful response",
1080
+ description: "Successful text response",
846
1081
  content: { "text/plain": { schema: { type: "string" } } }
847
1082
  };
848
1083
  }
@@ -853,7 +1088,18 @@ function analyzeHandler(handler) {
853
1088
  };
854
1089
  }
855
1090
  if (handlerSource.includes("ctx.redirect(")) {
856
- responses["302"] = { description: "Redirect" };
1091
+ let hasSpecificRedirect = false;
1092
+ const redirectMatches = Array.from(handlerSource.matchAll(/ctx\.redirect\([^,]+,\s*(\d{3})\)/g));
1093
+ for (const match of redirectMatches) {
1094
+ const status = match[1];
1095
+ if (/^30[12378]$/.test(status)) {
1096
+ responses[status] = { description: `Redirect (${status})` };
1097
+ hasSpecificRedirect = true;
1098
+ }
1099
+ }
1100
+ if (!hasSpecificRedirect) {
1101
+ responses["302"] = { description: "Redirect" };
1102
+ }
857
1103
  }
858
1104
  if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
859
1105
  responses["200"] = {
@@ -861,7 +1107,8 @@ function analyzeHandler(handler) {
861
1107
  content: { "application/json": { schema: { type: "object" } } }
862
1108
  };
863
1109
  }
864
- for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
1110
+ const errorStatusMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.ERROR_STATUS));
1111
+ for (const match of errorStatusMatches) {
865
1112
  const statusCode = match[1];
866
1113
  if (statusCode && statusCode !== "200") {
867
1114
  responses[statusCode] = { description: `Error response (${statusCode})` };
@@ -872,6 +1119,52 @@ function analyzeHandler(handler) {
872
1119
  }
873
1120
  return { inferredSpec };
874
1121
  }
1122
+ async function getAstRoutes(applications) {
1123
+ const astRoutes = [];
1124
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
1125
+ if (seen.has(app.name)) return [];
1126
+ const newSeen = new Set(seen);
1127
+ newSeen.add(app.name);
1128
+ const expanded = [];
1129
+ for (const route of app.routes) {
1130
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1131
+ const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1132
+ let joined = cleanPrefix + cleanPath;
1133
+ if (joined.length > 1 && joined.endsWith("/")) {
1134
+ joined = joined.slice(0, -1);
1135
+ }
1136
+ expanded.push({
1137
+ ...route,
1138
+ path: joined || "/"
1139
+ });
1140
+ }
1141
+ if (app.mounted) {
1142
+ for (const mount of app.mounted) {
1143
+ const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
1144
+ if (targetApp) {
1145
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1146
+ const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
1147
+ expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
1148
+ }
1149
+ }
1150
+ }
1151
+ return expanded;
1152
+ };
1153
+ applications.forEach((app) => {
1154
+ astRoutes.push(...getExpandedRoutes(app));
1155
+ });
1156
+ const dedupedRoutes = /* @__PURE__ */ new Map();
1157
+ for (const route of astRoutes) {
1158
+ const key = `${route.method.toUpperCase()}:${route.path}`;
1159
+ let score = 0;
1160
+ if (route.responseSchema) score += 10;
1161
+ if (route.handlerSource) score += 5;
1162
+ if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
1163
+ dedupedRoutes.set(key, { route, score });
1164
+ }
1165
+ }
1166
+ return Array.from(dedupedRoutes.values()).map((v) => v.route);
1167
+ }
875
1168
  async function generateOpenApi(rootRouter, options = {}) {
876
1169
  const paths = {};
877
1170
  const tagGroups = /* @__PURE__ */ new Map();
@@ -879,61 +1172,11 @@ async function generateOpenApi(rootRouter, options = {}) {
879
1172
  const defaultTagName = options.defaultTag || "Application";
880
1173
  let astRoutes = [];
881
1174
  try {
882
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-z-7AoFRC.cjs"));
1175
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-Bei1sVWp.cjs"));
883
1176
  const analyzer = new OpenAPIAnalyzer(process.cwd());
884
1177
  const { applications } = await analyzer.analyze();
885
- const appMap = /* @__PURE__ */ new Map();
886
- applications.forEach((app) => {
887
- appMap.set(app.name, app);
888
- if (app.name !== app.className) {
889
- appMap.set(app.className, app);
890
- }
891
- });
892
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
893
- if (seen.has(app.name)) return [];
894
- const newSeen = new Set(seen);
895
- newSeen.add(app.name);
896
- const expanded = [];
897
- for (const route of app.routes) {
898
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
899
- const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
900
- let joined = cleanPrefix + cleanPath;
901
- if (joined.length > 1 && joined.endsWith("/")) {
902
- joined = joined.slice(0, -1);
903
- }
904
- expanded.push({
905
- ...route,
906
- path: joined || "/"
907
- });
908
- }
909
- if (app.mounted) {
910
- for (const mount of app.mounted) {
911
- const targetApp = appMap.get(mount.target);
912
- if (targetApp) {
913
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
914
- const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
915
- expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
916
- }
917
- }
918
- }
919
- return expanded;
920
- };
921
- applications.forEach((app) => {
922
- astRoutes.push(...getExpandedRoutes(app));
923
- });
924
- const dedupedRoutes = /* @__PURE__ */ new Map();
925
- for (const route of astRoutes) {
926
- const key = `${route.method.toUpperCase()}:${route.path}`;
927
- let score = 0;
928
- if (route.responseSchema) score += 10;
929
- if (route.handlerSource) score += 5;
930
- if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
931
- dedupedRoutes.set(key, { route, score });
932
- }
933
- }
934
- astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
1178
+ astRoutes = await getAstRoutes(applications);
935
1179
  } catch (e) {
936
- console.warn("OpenAPI AST analysis failed or skipped:", e);
937
1180
  }
938
1181
  const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
939
1182
  let group = currentGroup;
@@ -990,33 +1233,15 @@ async function generateOpenApi(rootRouter, options = {}) {
990
1233
  (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
991
1234
  );
992
1235
  if (!astMatch) {
993
- let runtimeSource = route.handler.toString();
994
- if (route.handler.originalHandler) {
995
- runtimeSource = route.handler.originalHandler.toString();
996
- }
1236
+ const runtimeSource = (route.handler.originalHandler || route.handler).toString();
997
1237
  const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
998
1238
  const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
999
1239
  astMatch = sameMethodRoutes.find((r) => {
1000
1240
  const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
1001
1241
  if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
1002
- const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
1003
- return match;
1242
+ return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
1004
1243
  });
1005
1244
  }
1006
- const potentialMatches = astRoutes.filter(
1007
- (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
1008
- );
1009
- if (potentialMatches.length > 1) {
1010
- const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
1011
- const preciseMatch = potentialMatches.find((r) => {
1012
- const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
1013
- const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
1014
- return match;
1015
- });
1016
- if (preciseMatch) {
1017
- astMatch = preciseMatch;
1018
- }
1019
- }
1020
1245
  if (astMatch) {
1021
1246
  if (astMatch.summary) operation.summary = astMatch.summary;
1022
1247
  if (astMatch.description) operation.description = astMatch.description;
@@ -1024,25 +1249,19 @@ async function generateOpenApi(rootRouter, options = {}) {
1024
1249
  if (astMatch.operationId) operation.operationId = astMatch.operationId;
1025
1250
  if (astMatch.requestTypes?.body) {
1026
1251
  operation.requestBody = {
1027
- content: {
1028
- "application/json": { schema: astMatch.requestTypes.body }
1029
- }
1252
+ content: { "application/json": { schema: astMatch.requestTypes.body } }
1030
1253
  };
1031
1254
  }
1032
1255
  if (astMatch.responseSchema) {
1033
1256
  operation.responses["200"] = {
1034
1257
  description: "Successful response",
1035
- content: {
1036
- "application/json": { schema: astMatch.responseSchema }
1037
- }
1258
+ content: { "application/json": { schema: astMatch.responseSchema } }
1038
1259
  };
1039
1260
  } else if (astMatch.responseType) {
1040
1261
  const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
1041
1262
  operation.responses["200"] = {
1042
1263
  description: "Successful response",
1043
- content: {
1044
- [contentType]: { schema: { type: astMatch.responseType } }
1045
- }
1264
+ content: { [contentType]: { schema: { type: astMatch.responseType } } }
1046
1265
  };
1047
1266
  }
1048
1267
  const params = [];
@@ -1093,15 +1312,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1093
1312
  deepMerge(operation, inferredSpec);
1094
1313
  }
1095
1314
  if (route.handlerSpec) {
1096
- const spec = route.handlerSpec;
1097
- if (spec.summary) operation.summary = spec.summary;
1098
- if (spec.description) operation.description = spec.description;
1099
- if (spec.operationId) operation.operationId = spec.operationId;
1100
- if (spec.tags) operation.tags = spec.tags;
1101
- if (spec.security) operation.security = spec.security;
1102
- if (spec.responses) {
1103
- operation.responses = { ...operation.responses, ...spec.responses };
1104
- }
1315
+ deepMerge(operation, route.handlerSpec);
1105
1316
  }
1106
1317
  if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
1107
1318
  if (operation.tags) {
@@ -1120,11 +1331,13 @@ async function generateOpenApi(rootRouter, options = {}) {
1120
1331
  paths[fullPath][methodLower] = operation;
1121
1332
  }
1122
1333
  }
1123
- for (const controller of router[$childControllers]) {
1334
+ const controllers = router[$childControllers];
1335
+ for (const controller of controllers) {
1124
1336
  const controllerName = controller.constructor.name || "UnknownController";
1125
1337
  tagGroups.get(group)?.add(controllerName);
1126
1338
  }
1127
- for (const child of router[$childRouters]) {
1339
+ const childRouters = router[$childRouters];
1340
+ for (const child of childRouters) {
1128
1341
  const mountPath = child[$mountPath];
1129
1342
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1130
1343
  const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
@@ -1134,7 +1347,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1134
1347
  };
1135
1348
  collect(rootRouter);
1136
1349
  const xTagGroups = [];
1137
- for (const [name, tags] of tagGroups) {
1350
+ for (const [name, tags] of tagGroups.entries()) {
1138
1351
  xTagGroups.push({ name, tags: Array.from(tags).sort() });
1139
1352
  }
1140
1353
  return {
@@ -1156,12 +1369,23 @@ function serveStatic(config, prefix) {
1156
1369
  let relative = ctx.path.slice(normalizedPrefix.length);
1157
1370
  if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
1158
1371
  if (relative.length === 0) relative = "/";
1159
- relative = decodeURIComponent(relative);
1160
- const requestPath = path.join(rootPath, relative);
1161
- if (!requestPath.startsWith(rootPath)) {
1372
+ if (relative.includes("\0")) {
1373
+ return ctx.json({ error: "Forbidden" }, 403);
1374
+ }
1375
+ try {
1376
+ relative = decodeURIComponent(relative);
1377
+ } catch (e) {
1378
+ return ctx.json({ error: "Bad Request" }, 400);
1379
+ }
1380
+ if (relative.includes("\0")) {
1381
+ return ctx.json({ error: "Forbidden" }, 403);
1382
+ }
1383
+ if (relative.includes("../") || relative.includes("..\\")) {
1162
1384
  return ctx.json({ error: "Forbidden" }, 403);
1163
1385
  }
1164
- if (requestPath.includes("\0")) {
1386
+ const requestPath = path.resolve(path.join(rootPath, relative));
1387
+ const normalizedRoot = path.resolve(rootPath);
1388
+ if (!requestPath.startsWith(normalizedRoot + path.sep) && requestPath !== normalizedRoot) {
1165
1389
  return ctx.json({ error: "Forbidden" }, 403);
1166
1390
  }
1167
1391
  if (config.hooks?.onRequest) {
@@ -1169,7 +1393,8 @@ function serveStatic(config, prefix) {
1169
1393
  if (res) return res;
1170
1394
  }
1171
1395
  if (config.exclude) {
1172
- for (const pattern of config.exclude) {
1396
+ for (let i = 0; i < config.exclude.length; i++) {
1397
+ const pattern = config.exclude[i];
1173
1398
  if (pattern instanceof RegExp) {
1174
1399
  if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
1175
1400
  } else if (typeof pattern === "string") {
@@ -1188,7 +1413,8 @@ function serveStatic(config, prefix) {
1188
1413
  stats = await promises$1.stat(requestPath);
1189
1414
  } catch (e) {
1190
1415
  if (config.extensions) {
1191
- for (const ext of config.extensions) {
1416
+ for (let i = 0; i < config.extensions.length; i++) {
1417
+ const ext = config.extensions[i];
1192
1418
  const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
1193
1419
  try {
1194
1420
  const s = await promises$1.stat(p);
@@ -1217,7 +1443,8 @@ function serveStatic(config, prefix) {
1217
1443
  indexes = [config.index];
1218
1444
  }
1219
1445
  let foundIndex = false;
1220
- for (const idx of indexes) {
1446
+ for (let i = 0; i < indexes.length; i++) {
1447
+ const idx = indexes[i];
1221
1448
  const idxPath = path.join(finalPath, idx);
1222
1449
  try {
1223
1450
  const idxStats = await promises$1.stat(idxPath);
@@ -1297,38 +1524,39 @@ class RouterTrie {
1297
1524
  };
1298
1525
  }
1299
1526
  insert(method, path2, handler) {
1300
- let node2 = this.root;
1527
+ let node = this.root;
1301
1528
  const segments = this.splitPath(path2);
1302
- for (const segment of segments) {
1529
+ for (let i = 0; i < segments.length; i++) {
1530
+ const segment = segments[i];
1303
1531
  if (segment === "**") {
1304
- if (!node2.recursiveChild) {
1305
- node2.recursiveChild = this.createNode();
1532
+ if (!node.recursiveChild) {
1533
+ node.recursiveChild = this.createNode();
1306
1534
  }
1307
- node2 = node2.recursiveChild;
1535
+ node = node.recursiveChild;
1308
1536
  } else if (segment === "*") {
1309
- if (!node2.wildcardChild) {
1310
- node2.wildcardChild = this.createNode();
1537
+ if (!node.wildcardChild) {
1538
+ node.wildcardChild = this.createNode();
1311
1539
  }
1312
- node2 = node2.wildcardChild;
1540
+ node = node.wildcardChild;
1313
1541
  } else if (segment.startsWith(":")) {
1314
1542
  const paramName = segment.slice(1);
1315
- if (!node2.paramChild) {
1316
- node2.paramChild = this.createNode();
1317
- node2.paramChild.paramName = paramName;
1543
+ if (!node.paramChild) {
1544
+ node.paramChild = this.createNode();
1545
+ node.paramChild.paramName = paramName;
1318
1546
  }
1319
- node2 = node2.paramChild;
1320
- node2.paramName = paramName;
1547
+ node = node.paramChild;
1548
+ node.paramName = paramName;
1321
1549
  } else {
1322
- if (!node2.children[segment]) {
1323
- node2.children[segment] = this.createNode();
1550
+ if (!node.children[segment]) {
1551
+ node.children[segment] = this.createNode();
1324
1552
  }
1325
- node2 = node2.children[segment];
1553
+ node = node.children[segment];
1326
1554
  }
1327
1555
  }
1328
- if (!node2.handlers) {
1329
- node2.handlers = {};
1556
+ if (!node.handlers) {
1557
+ node.handlers = {};
1330
1558
  }
1331
- node2.handlers[method] = handler;
1559
+ node.handlers[method] = handler;
1332
1560
  }
1333
1561
  search(method, path2) {
1334
1562
  const segments = this.splitPath(path2);
@@ -1345,34 +1573,34 @@ class RouterTrie {
1345
1573
  }
1346
1574
  return null;
1347
1575
  }
1348
- findNode(node2, segments, index, params) {
1576
+ findNode(node, segments, index, params) {
1349
1577
  if (index === segments.length) {
1350
- if (node2.handlers) return node2;
1351
- if (node2.recursiveChild && node2.recursiveChild.handlers) {
1352
- return node2.recursiveChild;
1578
+ if (node.handlers) return node;
1579
+ if (node.recursiveChild && node.recursiveChild.handlers) {
1580
+ return node.recursiveChild;
1353
1581
  }
1354
1582
  return null;
1355
1583
  }
1356
1584
  const segment = segments[index];
1357
- const child = node2.children[segment];
1585
+ const child = node.children[segment];
1358
1586
  if (child) {
1359
1587
  const result = this.findNode(child, segments, index + 1, params);
1360
1588
  if (result) return result;
1361
1589
  }
1362
- if (node2.paramChild) {
1363
- params[node2.paramChild.paramName] = segment;
1364
- const result = this.findNode(node2.paramChild, segments, index + 1, params);
1590
+ if (node.paramChild) {
1591
+ params[node.paramChild.paramName] = segment;
1592
+ const result = this.findNode(node.paramChild, segments, index + 1, params);
1365
1593
  if (result) return result;
1366
- delete params[node2.paramChild.paramName];
1594
+ delete params[node.paramChild.paramName];
1367
1595
  }
1368
- if (node2.wildcardChild) {
1369
- const result = this.findNode(node2.wildcardChild, segments, index + 1, params);
1596
+ if (node.wildcardChild) {
1597
+ const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1370
1598
  if (result) return result;
1371
1599
  }
1372
- if (node2.recursiveChild) {
1600
+ if (node.recursiveChild) {
1373
1601
  const remaining = segments.length - index;
1374
1602
  for (let k = 0; k <= remaining; k++) {
1375
- const result = this.findNode(node2.recursiveChild, segments, index + k, params);
1603
+ const result = this.findNode(node.recursiveChild, segments, index + k, params);
1376
1604
  if (result) return result;
1377
1605
  }
1378
1606
  }
@@ -1386,40 +1614,68 @@ class RouterTrie {
1386
1614
  }
1387
1615
  }
1388
1616
  const asyncContext = new node_async_hooks.AsyncLocalStorage();
1389
- const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1390
- const db = new surrealdb.Surreal({
1391
- engines: node.createNodeEngines()
1392
- });
1393
- const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
1394
- return db.query(`
1395
- DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1396
- DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1397
- DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1398
- DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1399
- DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1400
- DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1401
- `);
1402
- });
1617
+ let db;
1618
+ let dbPromise = null;
1619
+ let RecordId;
1620
+ async function ensureDb() {
1621
+ if (db) return db;
1622
+ if (dbPromise) return dbPromise;
1623
+ dbPromise = (async () => {
1624
+ try {
1625
+ const { createNodeEngines } = await import("@surrealdb/node");
1626
+ const surreal = await import("surrealdb");
1627
+ const Surreal = surreal.Surreal;
1628
+ RecordId = surreal.RecordId;
1629
+ const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1630
+ const _db = new Surreal({
1631
+ engines: createNodeEngines()
1632
+ });
1633
+ await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1634
+ await _db.query(`
1635
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1636
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1637
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1638
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1639
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1640
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1641
+ `);
1642
+ db = _db;
1643
+ return db;
1644
+ } catch (e) {
1645
+ dbPromise = null;
1646
+ if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1647
+ throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1648
+ }
1649
+ throw e;
1650
+ }
1651
+ })();
1652
+ return dbPromise;
1653
+ }
1403
1654
  const datastore = {
1404
- get(store, key) {
1405
- return db.select(new surrealdb.RecordId(store, key));
1655
+ async get(store, key) {
1656
+ await ensureDb();
1657
+ return db.select(new RecordId(store, key));
1406
1658
  },
1407
- set(store, key, value) {
1408
- return db.create(new surrealdb.RecordId(store, key)).content(value);
1659
+ async set(store, key, value) {
1660
+ await ensureDb();
1661
+ return db.create(new RecordId(store, key)).content(value);
1409
1662
  },
1410
1663
  async query(query, vars) {
1664
+ await ensureDb();
1411
1665
  try {
1412
- const r = await db.query(query, vars).collect();
1413
- return r;
1666
+ const r = await db.query(query, vars);
1667
+ return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
1414
1668
  } catch (e) {
1415
1669
  console.error("DS ERROR:", e);
1416
1670
  throw e;
1417
1671
  }
1418
1672
  },
1419
- ready
1673
+ get ready() {
1674
+ return ensureDb().then(() => void 0);
1675
+ }
1420
1676
  };
1421
1677
  process.on("exit", async () => {
1422
- await db.close();
1678
+ if (db) await db.close();
1423
1679
  });
1424
1680
  const tracer = api.trace.getTracer("shokupan.middleware");
1425
1681
  function traceHandler(fn, name) {
@@ -1492,6 +1748,8 @@ class ShokupanRouter {
1492
1748
  [$parent] = null;
1493
1749
  [$childRouters] = [];
1494
1750
  [$childControllers] = [];
1751
+ hookCache = /* @__PURE__ */ new Map();
1752
+ hooksInitialized = false;
1495
1753
  middleware = [];
1496
1754
  get rootConfig() {
1497
1755
  return this[$appRoot]?.applicationConfig;
@@ -1509,7 +1767,8 @@ class ShokupanRouter {
1509
1767
  getComponentRegistry() {
1510
1768
  const controllerRoutesMap = /* @__PURE__ */ new Map();
1511
1769
  const localRoutes = [];
1512
- for (const r of this[$routes]) {
1770
+ for (let i = 0; i < this[$routes].length; i++) {
1771
+ const r = this[$routes][i];
1513
1772
  const entry = {
1514
1773
  type: "route",
1515
1774
  path: r.path,
@@ -1646,7 +1905,8 @@ class ShokupanRouter {
1646
1905
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1647
1906
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1648
1907
  let routesAttached = 0;
1649
- for (const name of Array.from(methods)) {
1908
+ for (let i = 0; i < Array.from(methods).length; i++) {
1909
+ const name = Array.from(methods)[i];
1650
1910
  if (name === "constructor") continue;
1651
1911
  if (["arguments", "caller", "callee"].includes(name)) continue;
1652
1912
  const originalHandler = instance[name];
@@ -1658,7 +1918,8 @@ class ShokupanRouter {
1658
1918
  method = config.method;
1659
1919
  subPath = config.path;
1660
1920
  } else {
1661
- for (const m of HTTPMethods) {
1921
+ for (let j = 0; j < HTTPMethods.length; j++) {
1922
+ const m = HTTPMethods[j];
1662
1923
  if (name.toUpperCase().startsWith(m)) {
1663
1924
  method = m;
1664
1925
  const rest = name.slice(m.length);
@@ -1673,8 +1934,8 @@ class ShokupanRouter {
1673
1934
  buffer = "";
1674
1935
  }
1675
1936
  };
1676
- for (let i = 0; i < rest.length; i++) {
1677
- const char = rest[i];
1937
+ for (let i2 = 0; i2 < rest.length; i2++) {
1938
+ const char = rest[i2];
1678
1939
  if (char === "$") {
1679
1940
  flush();
1680
1941
  subPath += "/:";
@@ -1712,7 +1973,8 @@ class ShokupanRouter {
1712
1973
  if (routeArgs?.length > 0) {
1713
1974
  args = [];
1714
1975
  const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1715
- for (const arg of sortedArgs) {
1976
+ for (let k = 0; k < sortedArgs.length; k++) {
1977
+ const arg = sortedArgs[k];
1716
1978
  switch (arg.type) {
1717
1979
  case RouteParamType.BODY:
1718
1980
  try {
@@ -1742,7 +2004,9 @@ class ShokupanRouter {
1742
2004
  args[arg.index] = vals.length > 1 ? vals : vals[0];
1743
2005
  } else {
1744
2006
  const query = {};
1745
- for (const key of url.searchParams.keys()) {
2007
+ const keys = Object.keys(url.searchParams);
2008
+ for (let k2 = 0; k2 < keys.length; k2++) {
2009
+ const key = keys[k2];
1746
2010
  const vals = url.searchParams.getAll(key);
1747
2011
  query[key] = vals.length > 1 ? vals : vals[0];
1748
2012
  }
@@ -1799,9 +2063,11 @@ class ShokupanRouter {
1799
2063
  path: r.path,
1800
2064
  handler: r.handler
1801
2065
  }));
1802
- for (const child of this[$childRouters]) {
2066
+ for (let i = 0; i < this[$childRouters].length; i++) {
2067
+ const child = this[$childRouters][i];
1803
2068
  const childRoutes = child.getRoutes();
1804
- for (const route of childRoutes) {
2069
+ for (let j = 0; j < childRoutes.length; j++) {
2070
+ const route = childRoutes[j];
1805
2071
  const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
1806
2072
  const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1807
2073
  const fullPath = cleanPrefix + cleanPath || "/";
@@ -1815,12 +2081,12 @@ class ShokupanRouter {
1815
2081
  return routes;
1816
2082
  }
1817
2083
  /**
1818
- * Makes a sub request to this router.
1819
- * This is useful for triggering other methods or route handlers.
2084
+ * Makes an internal request through this router's full routing pipeline.
2085
+ * This is useful for calling other routes internally and supports streaming responses.
1820
2086
  * @param options The request options.
1821
- * @returns The response.
2087
+ * @returns The raw Response object.
1822
2088
  */
1823
- async subRequest(arg) {
2089
+ async internalRequest(arg) {
1824
2090
  const options = typeof arg === "string" ? { path: arg } : arg;
1825
2091
  const store = asyncContext.getStore();
1826
2092
  store?.get("req");
@@ -1839,9 +2105,10 @@ class ShokupanRouter {
1839
2105
  return this.root[$dispatch](req);
1840
2106
  }
1841
2107
  /**
1842
- * Processes a request directly.
2108
+ * Processes a request for testing purposes.
2109
+ * Returns a simplified { status, headers, data } object instead of a Response.
1843
2110
  */
1844
- async processRequest(options) {
2111
+ async testRequest(options) {
1845
2112
  let url = options.url || options.path || "/";
1846
2113
  if (!url.startsWith("http")) {
1847
2114
  const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
@@ -1850,7 +2117,9 @@ class ShokupanRouter {
1850
2117
  }
1851
2118
  if (options.query) {
1852
2119
  const u = new URL(url);
1853
- for (const [k, v] of Object.entries(options.query)) {
2120
+ const entries = Object.entries(options.query);
2121
+ for (let i = 0; i < entries.length; i++) {
2122
+ const [k, v] = entries[i];
1854
2123
  u.searchParams.set(k, v);
1855
2124
  }
1856
2125
  url = u.toString();
@@ -1895,28 +2164,17 @@ class ShokupanRouter {
1895
2164
  data: result
1896
2165
  };
1897
2166
  }
1898
- applyRouterHooks(match) {
1899
- if (!this.config?.hooks) return match;
1900
- const hooks = this.config.hooks;
1901
- return {
1902
- ...match,
1903
- handler: this.wrapWithHooks(match.handler, hooks)
1904
- };
1905
- }
1906
- wrapWithHooks(handler, hooks) {
1907
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
1908
- const hasStart = hookList.some((h) => !!h.onRequestStart);
1909
- const hasEnd = hookList.some((h) => !!h.onRequestEnd);
1910
- const hasError = hookList.some((h) => !!h.onError);
2167
+ wrapWithHooks(handler) {
2168
+ if (!this.hooksInitialized) {
2169
+ this.ensureHooksInitialized();
2170
+ }
2171
+ const hasStart = this.hookCache.get("onRequestStart")?.length > 0;
2172
+ const hasEnd = this.hookCache.get("onRequestEnd")?.length > 0;
2173
+ const hasError = this.hookCache.get("onError")?.length > 0;
1911
2174
  if (!hasStart && !hasEnd && !hasError) return handler;
1912
2175
  const originalHandler = handler;
1913
2176
  const wrapped = async (ctx) => {
1914
- if (hasStart) {
1915
- for (let i = 0; i < hookList.length; i++) {
1916
- const h = hookList[i];
1917
- if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
1918
- }
1919
- }
2177
+ await this.runHooks("onRequestStart", ctx);
1920
2178
  const debug = ctx._debug;
1921
2179
  let debugId;
1922
2180
  let previousNode;
@@ -1930,17 +2188,11 @@ class ShokupanRouter {
1930
2188
  try {
1931
2189
  const res = await originalHandler(ctx);
1932
2190
  debug?.trackStep(debugId, "handler", performance.now() - start, "success");
1933
- for (let i = 0; i < hookList.length; i++) {
1934
- const h = hookList[i];
1935
- if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
1936
- }
2191
+ await this.runHooks("onRequestEnd", ctx);
1937
2192
  return res;
1938
2193
  } catch (err) {
1939
2194
  debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
1940
- for (let i = 0; i < hookList.length; i++) {
1941
- const h = hookList[i];
1942
- if (typeof h.onError === "function") await h.onError(err, ctx);
1943
- }
2195
+ await this.runHooks("onError", ctx, err);
1944
2196
  throw err;
1945
2197
  } finally {
1946
2198
  if (debug && previousNode) debug.setNode(previousNode);
@@ -1962,18 +2214,19 @@ class ShokupanRouter {
1962
2214
  result = this.trie.search("GET", path2);
1963
2215
  if (result) return result;
1964
2216
  }
1965
- for (const child of this[$childRouters]) {
2217
+ for (let i = 0; i < this[$childRouters].length; i++) {
2218
+ const child = this[$childRouters][i];
1966
2219
  const prefix = child[$mountPath];
1967
2220
  if (path2 === prefix || path2.startsWith(prefix + "/")) {
1968
2221
  const subPath = path2.slice(prefix.length) || "/";
1969
2222
  const match = child.find(method, subPath);
1970
- if (match) return this.applyRouterHooks(match);
2223
+ if (match) return match;
1971
2224
  }
1972
2225
  if (prefix.endsWith("/")) {
1973
2226
  if (path2.startsWith(prefix)) {
1974
2227
  const subPath = path2.slice(prefix.length) || "/";
1975
2228
  const match = child.find(method, subPath);
1976
- if (match) return this.applyRouterHooks(match);
2229
+ if (match) return match;
1977
2230
  }
1978
2231
  }
1979
2232
  }
@@ -1981,10 +2234,13 @@ class ShokupanRouter {
1981
2234
  }
1982
2235
  parsePath(path2) {
1983
2236
  const keys = [];
2237
+ if (path2.length > 2048) {
2238
+ throw new Error("Path too long");
2239
+ }
1984
2240
  const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
1985
2241
  keys.push(key);
1986
- return "([^/]+)";
1987
- }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
2242
+ return "([^/]{1,255})";
2243
+ }).replace(/\*\*/g, ".{0,1000}").replace(/\*/g, "[^/]{1,255}");
1988
2244
  return {
1989
2245
  regex: new RegExp(`^${pattern}$`),
1990
2246
  keys
@@ -1995,17 +2251,23 @@ class ShokupanRouter {
1995
2251
  /**
1996
2252
  * Adds a route to the router.
1997
2253
  *
1998
- * @param method - HTTP method
1999
- * @param path - URL path
2000
- * @param spec - OpenAPI specification for the route
2001
- * @param handler - Route handler function
2002
- * @param requestTimeout - Timeout for this route in milliseconds
2254
+ * @param arg - Route configuration object
2255
+ * @param arg.method - HTTP method
2256
+ * @param arg.path - URL path
2257
+ * @param arg.spec - OpenAPI specification for the route
2258
+ * @param arg.handler - Route handler function
2259
+ * @param arg.regex - Custom regex for path matching
2260
+ * @param arg.group - Group for the route
2261
+ * @param arg.requestTimeout - Timeout for this route in milliseconds
2262
+ * @param arg.renderer - JSX renderer for the route
2263
+ * @param arg.controller - Controller for the route
2003
2264
  */
2004
2265
  add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
2005
2266
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
2006
2267
  if (this.currentGuards.length > 0) {
2007
2268
  spec = spec || {};
2008
- for (const guard of this.currentGuards) {
2269
+ for (let i = 0; i < this.currentGuards.length; i++) {
2270
+ const guard = this.currentGuards[i];
2009
2271
  if (guard.spec) {
2010
2272
  if (guard.spec.responses) {
2011
2273
  spec.responses = spec.responses || {};
@@ -2034,7 +2296,8 @@ class ShokupanRouter {
2034
2296
  if (routeGuards.length > 0) {
2035
2297
  const innerHandler = wrappedHandler;
2036
2298
  wrappedHandler = async (ctx) => {
2037
- for (const guard of routeGuards) {
2299
+ for (let i = 0; i < routeGuards.length; i++) {
2300
+ const guard = routeGuards[i];
2038
2301
  let guardPassed = false;
2039
2302
  let nextCalled = false;
2040
2303
  const next = () => {
@@ -2128,7 +2391,7 @@ class ShokupanRouter {
2128
2391
  wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2129
2392
  let bakedHandler = wrappedHandler;
2130
2393
  if (this.config?.hooks) {
2131
- bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2394
+ bakedHandler = this.wrapWithHooks(wrappedHandler);
2132
2395
  }
2133
2396
  this[$routes].push({
2134
2397
  method,
@@ -2285,6 +2548,67 @@ class ShokupanRouter {
2285
2548
  generateApiSpec(options = {}) {
2286
2549
  return generateOpenApi(this, options);
2287
2550
  }
2551
+ ensureHooksInitialized() {
2552
+ const hooks = this.config?.hooks;
2553
+ if (hooks) {
2554
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2555
+ const hookTypes = [
2556
+ "onRequestStart",
2557
+ "onRequestEnd",
2558
+ "onResponseStart",
2559
+ "onResponseEnd",
2560
+ "onError",
2561
+ "beforeValidate",
2562
+ "afterValidate",
2563
+ "onRequestTimeout",
2564
+ "onReadTimeout",
2565
+ "onWriteTimeout"
2566
+ ];
2567
+ for (let i = 0; i < hookTypes.length; i++) {
2568
+ const type = hookTypes[i];
2569
+ const fns = [];
2570
+ for (let j = 0; j < hookList.length; j++) {
2571
+ const h = hookList[j];
2572
+ if (h[type]) fns.push(h[type]);
2573
+ }
2574
+ if (fns.length > 0) {
2575
+ this.hookCache.set(type, fns);
2576
+ }
2577
+ }
2578
+ }
2579
+ this.hooksInitialized = true;
2580
+ }
2581
+ async runHooks(name, ...args) {
2582
+ if (!this.hooksInitialized) {
2583
+ this.ensureHooksInitialized();
2584
+ }
2585
+ const fns = this.hookCache.get(name);
2586
+ if (!fns) return;
2587
+ const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2588
+ const debug = ctx?._debug;
2589
+ if (debug) {
2590
+ await Promise.all(fns.map(async (fn, index) => {
2591
+ const hookId = `hook_${name}_${fn.name || index}`;
2592
+ const previousNode = debug.getCurrentNode();
2593
+ debug.trackEdge(previousNode, hookId);
2594
+ debug.setNode(hookId);
2595
+ const start = performance.now();
2596
+ try {
2597
+ await fn(...args);
2598
+ const duration = performance.now() - start;
2599
+ debug.trackStep(hookId, "hook", duration, "success");
2600
+ } catch (error) {
2601
+ const duration = performance.now() - start;
2602
+ debug.trackStep(hookId, "hook", duration, "error", error);
2603
+ throw error;
2604
+ } finally {
2605
+ if (previousNode) debug.setNode(previousNode);
2606
+ }
2607
+ }));
2608
+ } else {
2609
+ await Promise.all(fns.map((fn) => fn(...args)));
2610
+ }
2611
+ }
2288
2612
  }
2289
2613
  class SystemCpuMonitor {
2290
2614
  constructor(intervalMs = 1e3) {
@@ -2342,15 +2666,13 @@ class Shokupan extends ShokupanRouter {
2342
2666
  openApiSpec;
2343
2667
  composedMiddleware;
2344
2668
  cpuMonitor;
2345
- hookCache = /* @__PURE__ */ new Map();
2346
- hooksInitialized = false;
2347
2669
  get logger() {
2348
2670
  return this.applicationConfig.logger;
2349
2671
  }
2350
2672
  constructor(applicationConfig = {}) {
2351
2673
  const config = Object.assign({}, defaults, applicationConfig);
2352
2674
  const { hooks, ...routerConfig } = config;
2353
- super(routerConfig);
2675
+ super({ ...routerConfig, hooks });
2354
2676
  this[$isApplication] = true;
2355
2677
  this[$appRoot] = this;
2356
2678
  this.applicationConfig = config;
@@ -2365,7 +2687,6 @@ class Shokupan extends ShokupanRouter {
2365
2687
  * Adds middleware to the application.
2366
2688
  */
2367
2689
  use(middleware) {
2368
- let trackedMiddleware = middleware;
2369
2690
  const { file, line } = getCallerInfo();
2370
2691
  if (!middleware.metadata) {
2371
2692
  middleware.metadata = {
@@ -2376,32 +2697,36 @@ class Shokupan extends ShokupanRouter {
2376
2697
  pluginName: middleware.pluginName
2377
2698
  };
2378
2699
  }
2379
- trackedMiddleware = async (ctx, next) => {
2380
- const c = ctx;
2381
- if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2382
- const metadata = middleware.metadata || {};
2383
- const start = performance.now();
2384
- const item = {
2385
- name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2386
- file: metadata.file || file,
2387
- line: metadata.line || line,
2388
- isBuiltin: metadata.isBuiltin,
2389
- startTime: start,
2390
- duration: -1
2391
- };
2392
- c.handlerStack.push(item);
2393
- try {
2394
- return await middleware(ctx, next);
2395
- } finally {
2396
- item.duration = performance.now() - start;
2700
+ if (this.applicationConfig.enableMiddlewareTracking) {
2701
+ const trackedMiddleware = async (ctx, next) => {
2702
+ const c = ctx;
2703
+ if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2704
+ const metadata = middleware.metadata || {};
2705
+ const start = performance.now();
2706
+ const item = {
2707
+ name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2708
+ file: metadata.file || file,
2709
+ line: metadata.line || line,
2710
+ isBuiltin: metadata.isBuiltin,
2711
+ startTime: start,
2712
+ duration: -1
2713
+ };
2714
+ c.handlerStack.push(item);
2715
+ try {
2716
+ return await middleware(ctx, next);
2717
+ } finally {
2718
+ item.duration = performance.now() - start;
2719
+ }
2397
2720
  }
2398
- }
2399
- return middleware(ctx, next);
2400
- };
2401
- trackedMiddleware.metadata = middleware.metadata;
2402
- Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2403
- trackedMiddleware.order = this.middleware.length;
2404
- this.middleware.push(trackedMiddleware);
2721
+ return middleware(ctx, next);
2722
+ };
2723
+ trackedMiddleware.metadata = middleware.metadata;
2724
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2725
+ trackedMiddleware.order = this.middleware.length;
2726
+ this.middleware.push(trackedMiddleware);
2727
+ } else {
2728
+ this.middleware.push(middleware);
2729
+ }
2405
2730
  return this;
2406
2731
  }
2407
2732
  startupHooks = [];
@@ -2432,17 +2757,13 @@ class Shokupan extends ShokupanRouter {
2432
2757
  if (finalPort < 0 || finalPort > 65535) {
2433
2758
  throw new Error("Invalid port number");
2434
2759
  }
2435
- for (const hook of this.startupHooks) {
2436
- await hook();
2437
- }
2760
+ await Promise.all(this.startupHooks.map((hook) => hook()));
2438
2761
  if (this.applicationConfig.enableOpenApiGen) {
2439
2762
  this.openApiSpec = await generateOpenApi(this);
2440
- for (const hook of this.specAvailableHooks) {
2441
- await hook(this.openApiSpec);
2442
- }
2763
+ await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
2443
2764
  }
2444
2765
  if (port === 0 && process.platform === "linux") ;
2445
- if (this.applicationConfig.autoBackpressureFeedback) {
2766
+ if (this.applicationConfig.autoBackpressureFeedback === true) {
2446
2767
  this.cpuMonitor = new SystemCpuMonitor();
2447
2768
  this.cpuMonitor.start();
2448
2769
  }
@@ -2470,7 +2791,7 @@ class Shokupan extends ShokupanRouter {
2470
2791
  };
2471
2792
  let factory = this.applicationConfig.serverFactory;
2472
2793
  if (!factory && typeof Bun === "undefined") {
2473
- const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-fVKP60e0.cjs"));
2794
+ const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-DFhwlK8e.cjs"));
2474
2795
  factory = createHttpServer();
2475
2796
  }
2476
2797
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2483,7 +2804,7 @@ class Shokupan extends ShokupanRouter {
2483
2804
  /**
2484
2805
  * Processes a request by wrapping the standard fetch method.
2485
2806
  */
2486
- async processRequest(options) {
2807
+ async testRequest(options) {
2487
2808
  let url = options.url || options.path || "/";
2488
2809
  if (!url.startsWith("http")) {
2489
2810
  const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
@@ -2492,7 +2813,9 @@ class Shokupan extends ShokupanRouter {
2492
2813
  }
2493
2814
  if (options.query) {
2494
2815
  const u = new URL(url);
2495
- for (const [k, v] of Object.entries(options.query)) {
2816
+ const entries = Object.entries(options.query);
2817
+ for (let i = 0; i < entries.length; i++) {
2818
+ const [k, v] = entries[i];
2496
2819
  u.searchParams.set(k, v);
2497
2820
  }
2498
2821
  url = u.toString();
@@ -2561,18 +2884,18 @@ class Shokupan extends ShokupanRouter {
2561
2884
  if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2562
2885
  const msg = "Too Many Requests (CPU Backpressure)";
2563
2886
  const res = ctx.text(msg, 429);
2564
- await this.executeHook("onResponseEnd", ctx, res);
2887
+ await this.runHooks("onResponseEnd", ctx, res);
2565
2888
  return res;
2566
2889
  }
2567
2890
  try {
2568
- if (this.hasHook("onRequestStart")) {
2569
- await this.executeHook("onRequestStart", ctx);
2570
- }
2891
+ await this.runHooks("onRequestStart", ctx);
2571
2892
  const fn = this.composedMiddleware ??= compose(this.middleware);
2572
2893
  const result = await fn(ctx, async () => {
2894
+ const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2573
2895
  const match = this.find(req.method, ctx.path);
2574
2896
  if (match) {
2575
2897
  ctx.params = match.params;
2898
+ await bodyParsing;
2576
2899
  return match.handler(ctx);
2577
2900
  }
2578
2901
  return null;
@@ -2595,12 +2918,8 @@ class Shokupan extends ShokupanRouter {
2595
2918
  } else {
2596
2919
  response = ctx.text(String(result));
2597
2920
  }
2598
- if (this.hasHook("onRequestEnd")) {
2599
- await this.executeHook("onRequestEnd", ctx);
2600
- }
2601
- if (this.hasHook("onResponseStart")) {
2602
- await this.executeHook("onResponseStart", ctx, response);
2603
- }
2921
+ await this.runHooks("onRequestEnd", ctx);
2922
+ await this.runHooks("onResponseStart", ctx, response);
2604
2923
  return response;
2605
2924
  } catch (err) {
2606
2925
  console.error(err);
@@ -2609,9 +2928,7 @@ class Shokupan extends ShokupanRouter {
2609
2928
  const status = err.status || err.statusCode || 500;
2610
2929
  const body = { error: err.message || "Internal Server Error" };
2611
2930
  if (err.errors) body.errors = err.errors;
2612
- if (this.hasHook("onError")) {
2613
- await this.executeHook("onError", err, ctx);
2614
- }
2931
+ await this.runHooks("onError", ctx, err);
2615
2932
  return ctx.json(body, status);
2616
2933
  }
2617
2934
  };
@@ -2622,9 +2939,7 @@ class Shokupan extends ShokupanRouter {
2622
2939
  const timeoutPromise = new Promise((_, reject) => {
2623
2940
  timeoutId = setTimeout(async () => {
2624
2941
  controller.abort();
2625
- if (this.hasHook("onRequestTimeout")) {
2626
- await this.executeHook("onRequestTimeout", ctx);
2627
- }
2942
+ await this.runHooks("onRequestTimeout", ctx);
2628
2943
  reject(new Error("Request Timeout"));
2629
2944
  }, timeoutMs);
2630
2945
  });
@@ -2637,56 +2952,10 @@ class Shokupan extends ShokupanRouter {
2637
2952
  console.error("Unexpected error in request execution:", err);
2638
2953
  return ctx.text("Internal Server Error", 500);
2639
2954
  }).then(async (res) => {
2640
- if (this.hasHook("onResponseEnd")) {
2641
- await this.executeHook("onResponseEnd", ctx, res);
2642
- }
2955
+ await this.runHooks("onResponseEnd", ctx, res);
2643
2956
  return res;
2644
2957
  });
2645
2958
  }
2646
- ensureHooksInitialized() {
2647
- const hooks = this.applicationConfig.hooks;
2648
- if (hooks) {
2649
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
2650
- const hookTypes = [
2651
- "onRequestStart",
2652
- "onRequestEnd",
2653
- "onResponseStart",
2654
- "onResponseEnd",
2655
- "onError",
2656
- "beforeValidate",
2657
- "afterValidate",
2658
- "onRequestTimeout",
2659
- "onReadTimeout",
2660
- "onWriteTimeout"
2661
- ];
2662
- for (const type of hookTypes) {
2663
- const fns = [];
2664
- for (const h of hookList) {
2665
- if (h[type]) fns.push(h[type]);
2666
- }
2667
- if (fns.length > 0) {
2668
- this.hookCache.set(type, fns);
2669
- }
2670
- }
2671
- }
2672
- this.hooksInitialized = true;
2673
- }
2674
- async executeHook(name, ...args) {
2675
- if (!this.hooksInitialized) {
2676
- this.ensureHooksInitialized();
2677
- }
2678
- const fns = this.hookCache.get(name);
2679
- if (!fns) return;
2680
- for (const fn of fns) {
2681
- await fn(...args);
2682
- }
2683
- }
2684
- hasHook(name) {
2685
- if (!this.hooksInitialized) {
2686
- this.ensureHooksInitialized();
2687
- }
2688
- return this.hookCache.has(name);
2689
- }
2690
2959
  }
2691
2960
  class AuthPlugin extends ShokupanRouter {
2692
2961
  constructor(authConfig) {
@@ -2734,7 +3003,9 @@ class AuthPlugin extends ShokupanRouter {
2734
3003
  return jwt;
2735
3004
  }
2736
3005
  init() {
2737
- for (const [providerName, providerConfig] of Object.entries(this.authConfig.providers)) {
3006
+ const providerEntries = Object.entries(this.authConfig.providers);
3007
+ for (let i = 0; i < providerEntries.length; i++) {
3008
+ const [providerName, providerConfig] = providerEntries[i];
2738
3009
  if (!providerConfig) continue;
2739
3010
  const provider = this.getProviderInstance(providerName, providerConfig);
2740
3011
  if (!provider) {
@@ -2757,9 +3028,10 @@ class AuthPlugin extends ShokupanRouter {
2757
3028
  } else {
2758
3029
  return ctx.text("Provider config error", 500);
2759
3030
  }
2760
- ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; Max-Age=600`);
3031
+ const isSecure = ctx.secure;
3032
+ ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
2761
3033
  if (codeVerifier) {
2762
- ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; Max-Age=600`);
3034
+ ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
2763
3035
  }
2764
3036
  return ctx.redirect(url.toString());
2765
3037
  });
@@ -2800,7 +3072,7 @@ class AuthPlugin extends ShokupanRouter {
2800
3072
  return ctx.json({ token: jwt, user });
2801
3073
  } catch (e) {
2802
3074
  console.error("Auth Error", e);
2803
- return ctx.text("Authentication failed: " + e.message + "\n" + e.stack, 500);
3075
+ return ctx.text("Authentication failed. Please try again.", 500);
2804
3076
  }
2805
3077
  });
2806
3078
  }
@@ -3011,14 +3283,21 @@ function Cors(options = {}) {
3011
3283
  const origin = ctx.headers.get("origin");
3012
3284
  const set = (k, v) => headers.set(k, v);
3013
3285
  const append = (k, v) => headers.append(k, v);
3286
+ if (origin === "null" && opts.origin !== "null") {
3287
+ return next();
3288
+ }
3014
3289
  if (opts.origin === "*") {
3015
3290
  set("Access-Control-Allow-Origin", "*");
3016
3291
  } else if (typeof opts.origin === "string") {
3017
3292
  set("Access-Control-Allow-Origin", opts.origin);
3018
3293
  } else if (Array.isArray(opts.origin)) {
3019
- if (origin && opts.origin.includes(origin)) {
3020
- set("Access-Control-Allow-Origin", origin);
3021
- append("Vary", "Origin");
3294
+ if (origin) {
3295
+ const normalizedOrigin = origin.toLowerCase();
3296
+ const normalizedAllowed = opts.origin.map((o) => o.toLowerCase());
3297
+ if (normalizedAllowed.includes(normalizedOrigin)) {
3298
+ set("Access-Control-Allow-Origin", origin);
3299
+ append("Vary", "Origin");
3300
+ }
3022
3301
  }
3023
3302
  } else if (typeof opts.origin === "function") {
3024
3303
  const allowed = opts.origin(ctx);
@@ -3062,7 +3341,9 @@ function Cors(options = {}) {
3062
3341
  }
3063
3342
  const response = await next();
3064
3343
  if (response instanceof Response) {
3065
- for (const [key, value] of headers.entries()) {
3344
+ const headerEntries = Array.from(headers.entries());
3345
+ for (let i = 0; i < headerEntries.length; i++) {
3346
+ const [key, value] = headerEntries[i];
3066
3347
  response.headers.set(key, value);
3067
3348
  }
3068
3349
  }
@@ -3132,6 +3413,8 @@ function useExpress(expressMiddleware) {
3132
3413
  });
3133
3414
  };
3134
3415
  }
3416
+ let plainToInstance;
3417
+ let validateOrReject;
3135
3418
  class ValidationError extends Error {
3136
3419
  constructor(errors) {
3137
3420
  super("Validation Error");
@@ -3196,9 +3479,21 @@ function isClass(schema) {
3196
3479
  }
3197
3480
  }
3198
3481
  async function validateClassValidator(schema, data) {
3199
- const object = classTransformer.plainToInstance(schema, data);
3482
+ if (!plainToInstance || !validateOrReject) {
3483
+ try {
3484
+ const ct = await import("class-transformer");
3485
+ const cv = await import("class-validator");
3486
+ plainToInstance = ct.plainToInstance;
3487
+ validateOrReject = cv.validateOrReject;
3488
+ } catch (e) {
3489
+ throw new Error(
3490
+ "class-transformer and class-validator are required for class-based validation. Install them with: bun add class-transformer class-validator reflect-metadata"
3491
+ );
3492
+ }
3493
+ }
3494
+ const object = plainToInstance(schema, data);
3200
3495
  try {
3201
- await classValidator.validateOrReject(object);
3496
+ await validateOrReject(object);
3202
3497
  return object;
3203
3498
  } catch (errors) {
3204
3499
  const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
@@ -3210,30 +3505,8 @@ async function validateClassValidator(schema, data) {
3210
3505
  }
3211
3506
  }
3212
3507
  const safelyGetBody = async (ctx) => {
3213
- const req = ctx.req;
3214
- if (req._bodyParsed) {
3215
- return req._bodyValue;
3216
- }
3217
3508
  try {
3218
- let data;
3219
- if (typeof req.json === "function") {
3220
- data = await req.json();
3221
- } else {
3222
- data = req.body;
3223
- if (typeof data === "string") {
3224
- try {
3225
- data = JSON.parse(data);
3226
- } catch {
3227
- }
3228
- }
3229
- }
3230
- req._bodyParsed = true;
3231
- req._bodyValue = data;
3232
- Object.defineProperty(req, "json", {
3233
- value: async () => req._bodyValue,
3234
- configurable: true
3235
- });
3236
- return data;
3509
+ return await ctx.body();
3237
3510
  } catch (e) {
3238
3511
  return {};
3239
3512
  }
@@ -3280,9 +3553,7 @@ function validate(config) {
3280
3553
  body = await safelyGetBody(ctx);
3281
3554
  dataToValidate.body = body;
3282
3555
  }
3283
- if (ctx.app?.hasHook("beforeValidate")) {
3284
- await ctx.app.executeHook("beforeValidate", ctx, dataToValidate);
3285
- }
3556
+ await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
3286
3557
  if (validators.params) {
3287
3558
  ctx.params = await validators.params(ctx.params);
3288
3559
  }
@@ -3298,21 +3569,20 @@ function validate(config) {
3298
3569
  if (validators.body) {
3299
3570
  const b = body ?? await safelyGetBody(ctx);
3300
3571
  validBody = await validators.body(b);
3572
+ ctx._cachedBody = validBody;
3301
3573
  const req = ctx.req;
3302
- req._bodyValue = validBody;
3303
3574
  Object.defineProperty(req, "json", {
3304
3575
  value: async () => validBody,
3576
+ writable: true,
3305
3577
  configurable: true
3306
3578
  });
3307
3579
  ctx.body = validBody;
3308
3580
  }
3309
- if (ctx.app?.hasHook("afterValidate")) {
3310
- const validatedData = { ...dataToValidate };
3311
- if (config.params) validatedData.params = ctx.params;
3312
- if (config.query) validatedData.query = validQuery;
3313
- if (config.body) validatedData.body = validBody;
3314
- await ctx.app?.executeHook("afterValidate", ctx, validatedData);
3315
- }
3581
+ const validatedData = { ...dataToValidate };
3582
+ if (config.params) validatedData.params = ctx.params;
3583
+ if (config.query) validatedData.query = validQuery;
3584
+ if (config.body) validatedData.body = validBody;
3585
+ await ctx.app.runHooks("afterValidate", ctx, validatedData);
3316
3586
  return next();
3317
3587
  };
3318
3588
  }
@@ -3335,12 +3605,14 @@ function openApiValidator() {
3335
3605
  if (cache.validators.has(ctx.path)) {
3336
3606
  matchPath = ctx.path;
3337
3607
  } else {
3338
- for (const [path2, { regex, paramNames }] of cache.paths) {
3608
+ const pathEntries = Array.from(cache.paths.entries());
3609
+ for (let i = 0; i < pathEntries.length; i++) {
3610
+ const [path2, { regex, paramNames }] = pathEntries[i];
3339
3611
  const match = regex.exec(ctx.path);
3340
3612
  if (match) {
3341
3613
  matchPath = path2;
3342
- paramNames.forEach((name, i) => {
3343
- matchParams[name] = match[i + 1];
3614
+ paramNames.forEach((name, i2) => {
3615
+ matchParams[name] = match[i2 + 1];
3344
3616
  });
3345
3617
  break;
3346
3618
  }
@@ -3397,7 +3669,9 @@ function openApiValidator() {
3397
3669
  function compileValidators(spec) {
3398
3670
  const validators = /* @__PURE__ */ new Map();
3399
3671
  const paths = /* @__PURE__ */ new Map();
3400
- for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
3672
+ const pathEntries = Object.entries(spec.paths || {});
3673
+ for (let i = 0; i < pathEntries.length; i++) {
3674
+ const [path2, pathItem] = pathEntries[i];
3401
3675
  if (path2.includes("{")) {
3402
3676
  const paramNames = [];
3403
3677
  const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
@@ -3410,7 +3684,9 @@ function compileValidators(spec) {
3410
3684
  });
3411
3685
  }
3412
3686
  const pathValidators = {};
3413
- for (const [method, operation] of Object.entries(pathItem)) {
3687
+ const methodEntries = Object.entries(pathItem);
3688
+ for (let k = 0; k < methodEntries.length; k++) {
3689
+ const [method, operation] = methodEntries[k];
3414
3690
  if (method === "parameters" || method === "summary" || method === "description") continue;
3415
3691
  const oper = operation;
3416
3692
  const opValidators = {};
@@ -3424,7 +3700,8 @@ function compileValidators(spec) {
3424
3700
  const queryRequired = [];
3425
3701
  const pathRequired = [];
3426
3702
  const headerRequired = [];
3427
- for (const param of parameters) {
3703
+ for (let j = 0; j < parameters.length; j++) {
3704
+ const param = parameters[j];
3428
3705
  if (param.in === "query") {
3429
3706
  queryProps[param.name] = param.schema || {};
3430
3707
  if (param.required) queryRequired.push(param.name);
@@ -3585,14 +3862,18 @@ function SecurityHeaders(options = {}) {
3585
3862
  if (opt === void 0 || opt === true) {
3586
3863
  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");
3587
3864
  } else if (typeof opt === "object") {
3588
- for (const [key, val] of Object.entries(opt)) {
3865
+ const optEntries = Object.entries(opt);
3866
+ for (let i = 0; i < optEntries.length; i++) {
3867
+ const [key, val] = optEntries[i];
3589
3868
  }
3590
3869
  }
3591
3870
  }
3592
3871
  if (options.hidePoweredBy !== false) ;
3593
3872
  const response = await next();
3594
3873
  if (response instanceof Response) {
3595
- for (const [k, v] of Object.entries(headers)) {
3874
+ const headerEntries = Object.entries(headers);
3875
+ for (let i = 0; i < headerEntries.length; i++) {
3876
+ const [k, v] = headerEntries[i];
3596
3877
  response.headers.set(k, v);
3597
3878
  }
3598
3879
  return response;
@@ -3678,7 +3959,9 @@ class MemoryStore extends events.EventEmitter {
3678
3959
  }
3679
3960
  all(cb) {
3680
3961
  const result = {};
3681
- for (const sid in this.sessions) {
3962
+ const sessionKeys = Object.keys(this.sessions);
3963
+ for (let i = 0; i < sessionKeys.length; i++) {
3964
+ const sid = sessionKeys[i];
3682
3965
  try {
3683
3966
  result[sid] = JSON.parse(this.sessions[sid]);
3684
3967
  } catch {
@@ -3701,11 +3984,17 @@ function unsign(input, secret) {
3701
3984
  if (typeof secret !== "string") throw new TypeError("Secret string must be provided.");
3702
3985
  const tentValue = input.slice(0, input.lastIndexOf("."));
3703
3986
  const expectedInput = sign(tentValue, secret);
3704
- const expectedBuffer = Buffer.from(expectedInput);
3705
- const inputBuffer = Buffer.from(input);
3706
- if (expectedBuffer.length !== inputBuffer.length) return false;
3707
- const valid = require("crypto").timingSafeEqual(expectedBuffer, inputBuffer);
3708
- return valid ? tentValue : false;
3987
+ const maxLength = Math.max(expectedInput.length, input.length);
3988
+ const paddedExpected = Buffer.alloc(maxLength);
3989
+ const paddedInput = Buffer.alloc(maxLength);
3990
+ Buffer.from(expectedInput).copy(paddedExpected);
3991
+ Buffer.from(input).copy(paddedInput);
3992
+ try {
3993
+ const valid = require("crypto").timingSafeEqual(paddedExpected, paddedInput);
3994
+ return valid ? tentValue : false;
3995
+ } catch {
3996
+ return false;
3997
+ }
3709
3998
  }
3710
3999
  function Session(options) {
3711
4000
  const store = options.store || new MemoryStore();
@@ -3764,7 +4053,9 @@ function Session(options) {
3764
4053
  sessObj.regenerate = (cb) => {
3765
4054
  store.destroy(sessObj.id, (err) => {
3766
4055
  sessionID = generateId(ctx);
3767
- for (const key in sessObj) {
4056
+ const keys = Object.keys(sessObj);
4057
+ for (let i = 0; i < keys.length; i++) {
4058
+ const key = keys[i];
3768
4059
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3769
4060
  delete sessObj[key];
3770
4061
  }
@@ -3779,7 +4070,9 @@ function Session(options) {
3779
4070
  store.get(sessObj.id, (err, sess2) => {
3780
4071
  if (err) return cb(err);
3781
4072
  if (!sess2) return cb(new Error("Session not found"));
3782
- for (const key in sessObj) {
4073
+ const keys = Object.keys(sessObj);
4074
+ for (let i = 0; i < keys.length; i++) {
4075
+ const key = keys[i];
3783
4076
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3784
4077
  delete sessObj[key];
3785
4078
  }