shokupan 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +4 -2
  2. package/dist/{openapi-analyzer-Bei1sVWp.cjs → analyzer-Bei1sVWp.cjs} +1 -1
  3. package/dist/analyzer-Bei1sVWp.cjs.map +1 -0
  4. package/dist/{openapi-analyzer-Ce_7JxZh.js → analyzer-Ce_7JxZh.js} +1 -1
  5. package/dist/analyzer-Ce_7JxZh.js.map +1 -0
  6. package/dist/cli.cjs +2 -2
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/cli.js.map +1 -1
  10. package/dist/context.d.ts +72 -11
  11. package/dist/{server-adapter-0xH174zz.js → http-server-0xH174zz.js} +1 -1
  12. package/dist/http-server-0xH174zz.js.map +1 -0
  13. package/dist/{server-adapter-DFhwlK8e.cjs → http-server-DFhwlK8e.cjs} +1 -1
  14. package/dist/http-server-DFhwlK8e.cjs.map +1 -0
  15. package/dist/index.cjs +1022 -801
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.ts +17 -17
  18. package/dist/index.js +1022 -800
  19. package/dist/index.js.map +1 -1
  20. package/dist/middleware.d.ts +1 -1
  21. package/dist/plugins/{auth.d.ts → application/auth.d.ts} +72 -3
  22. package/dist/plugins/application/cluster.d.ts +33 -0
  23. package/dist/plugins/{failed-request-recorder.d.ts → application/dashboard/failed-request-recorder.d.ts} +1 -1
  24. package/dist/plugins/{debugview → application/dashboard}/plugin.d.ts +13 -6
  25. package/dist/plugins/{server-adapter.d.ts → application/http-server.d.ts} +1 -1
  26. package/dist/plugins/{idempotency → application/idempotency}/plugin.d.ts +7 -1
  27. package/dist/plugins/{openapi.d.ts → application/openapi/openapi.d.ts} +2 -2
  28. package/dist/plugins/application/scalar.d.ts +36 -0
  29. package/dist/plugins/middleware/compression.d.ts +17 -0
  30. package/dist/plugins/middleware/cors.d.ts +34 -0
  31. package/dist/plugins/{express.d.ts → middleware/express.d.ts} +1 -1
  32. package/dist/plugins/{openapi-validator.d.ts → middleware/openapi-validator.d.ts} +2 -2
  33. package/dist/plugins/middleware/proxy.d.ts +37 -0
  34. package/dist/plugins/middleware/rate-limit.d.ts +58 -0
  35. package/dist/plugins/{security-headers.d.ts → middleware/security-headers.d.ts} +51 -1
  36. package/dist/plugins/{serve-static.d.ts → middleware/serve-static.d.ts} +1 -1
  37. package/dist/plugins/{session.d.ts → middleware/session.d.ts} +89 -3
  38. package/dist/plugins/{validation.d.ts → middleware/validation.d.ts} +6 -1
  39. package/dist/router.d.ts +99 -40
  40. package/dist/shokupan.d.ts +74 -4
  41. package/dist/util/async-hooks.d.ts +8 -2
  42. package/dist/{decorators.d.ts → util/decorators.d.ts} +1 -1
  43. package/dist/util/http-status.d.ts +2 -0
  44. package/dist/util/instrumentation.d.ts +1 -1
  45. package/dist/{router → util}/trie.d.ts +1 -1
  46. package/dist/{types.d.ts → util/types.d.ts} +41 -2
  47. package/package.json +5 -5
  48. package/dist/openapi-analyzer-Bei1sVWp.cjs.map +0 -1
  49. package/dist/openapi-analyzer-Ce_7JxZh.js.map +0 -1
  50. package/dist/plugins/compression.d.ts +0 -5
  51. package/dist/plugins/cors.d.ts +0 -11
  52. package/dist/plugins/proxy.d.ts +0 -9
  53. package/dist/plugins/rate-limit.d.ts +0 -14
  54. package/dist/plugins/scalar.d.ts +0 -15
  55. package/dist/server-adapter-0xH174zz.js.map +0 -1
  56. package/dist/server-adapter-DFhwlK8e.cjs.map +0 -1
  57. /package/dist/{analysis/openapi-analyzer.d.ts → plugins/application/openapi/analyzer.d.ts} +0 -0
  58. /package/dist/{di.d.ts → util/di.d.ts} +0 -0
  59. /package/dist/{request.d.ts → util/request.d.ts} +0 -0
  60. /package/dist/{response.d.ts → util/response.d.ts} +0 -0
  61. /package/dist/{symbol.d.ts → util/symbol.d.ts} +0 -0
package/dist/index.cjs CHANGED
@@ -23,18 +23,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
25
25
  const promises = require("node:fs/promises");
26
+ const api = require("@opentelemetry/api");
27
+ const node_async_hooks = require("node:async_hooks");
26
28
  const eta$2 = require("eta");
27
29
  const promises$1 = require("fs/promises");
28
30
  const path = require("path");
29
- const node_async_hooks = require("node:async_hooks");
30
- const api = require("@opentelemetry/api");
31
31
  const os = require("node:os");
32
32
  const arctic = require("arctic");
33
33
  const jose = require("jose");
34
+ const cluster = require("node:cluster");
35
+ const net = require("node:net");
36
+ const analyzer = require("./analyzer-Bei1sVWp.cjs");
34
37
  const zlib = require("node:zlib");
35
38
  const Ajv = require("ajv");
36
39
  const addFormats = require("ajv-formats");
37
- const openapiAnalyzer = require("./openapi-analyzer-Bei1sVWp.cjs");
38
40
  const crypto = require("crypto");
39
41
  const events = require("events");
40
42
  function _interopNamespaceDefault(e) {
@@ -56,69 +58,6 @@ function _interopNamespaceDefault(e) {
56
58
  const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
57
59
  const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
58
60
  const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
59
- class ShokupanResponse {
60
- _headers = null;
61
- _status = 200;
62
- /**
63
- * Get the current headers
64
- */
65
- get headers() {
66
- if (!this._headers) this._headers = new Headers();
67
- return this._headers;
68
- }
69
- /**
70
- * Get the current status code
71
- */
72
- get status() {
73
- return this._status;
74
- }
75
- /**
76
- * Set the status code
77
- */
78
- set status(code) {
79
- this._status = code;
80
- }
81
- /**
82
- * Set a response header
83
- * @param key Header name
84
- * @param value Header value
85
- */
86
- set(key, value) {
87
- if (!this._headers) this._headers = new Headers();
88
- this._headers.set(key, value);
89
- return this;
90
- }
91
- /**
92
- * Append to a response header
93
- * @param key Header name
94
- * @param value Header value
95
- */
96
- append(key, value) {
97
- if (!this._headers) this._headers = new Headers();
98
- this._headers.append(key, value);
99
- return this;
100
- }
101
- /**
102
- * Get a response header value
103
- * @param key Header name
104
- */
105
- get(key) {
106
- return this._headers?.get(key) || null;
107
- }
108
- /**
109
- * Check if a header exists
110
- * @param key Header name
111
- */
112
- has(key) {
113
- return this._headers?.has(key) || false;
114
- }
115
- /**
116
- * Internal: check if headers have been initialized/modified
117
- */
118
- get hasPopulatedHeaders() {
119
- return this._headers !== null;
120
- }
121
- }
122
61
  const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
123
62
  100,
124
63
  101,
@@ -185,6 +124,78 @@ const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
185
124
  511
186
125
  ]);
187
126
  const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
127
+ class ShokupanResponse {
128
+ _headers = null;
129
+ _status = 200;
130
+ /**
131
+ * Get the current headers
132
+ */
133
+ get headers() {
134
+ if (!this._headers) this._headers = new Headers();
135
+ return this._headers;
136
+ }
137
+ /**
138
+ * Get the current status code
139
+ */
140
+ get status() {
141
+ return this._status;
142
+ }
143
+ /**
144
+ * Set the status code
145
+ */
146
+ set status(code) {
147
+ this._status = code;
148
+ }
149
+ /**
150
+ * Set a response header
151
+ * @param key Header name
152
+ * @param value Header value
153
+ */
154
+ set(key, value) {
155
+ if (!this._headers) this._headers = new Headers();
156
+ this._headers.set(key, value);
157
+ return this;
158
+ }
159
+ /**
160
+ * Append to a response header
161
+ * @param key Header name
162
+ * @param value Header value
163
+ */
164
+ append(key, value) {
165
+ if (!this._headers) this._headers = new Headers();
166
+ this._headers.append(key, value);
167
+ return this;
168
+ }
169
+ /**
170
+ * Get a response header value
171
+ * @param key Header name
172
+ */
173
+ get(key) {
174
+ return this._headers?.get(key) || null;
175
+ }
176
+ /**
177
+ * Check if a header exists
178
+ * @param key Header name
179
+ */
180
+ has(key) {
181
+ return this._headers?.has(key) || false;
182
+ }
183
+ /**
184
+ * Internal: check if headers have been initialized/modified
185
+ */
186
+ get hasPopulatedHeaders() {
187
+ return this._headers !== null;
188
+ }
189
+ }
190
+ function isValidCookieDomain(domain, currentHost) {
191
+ const hostWithoutPort = currentHost.split(":")[0];
192
+ if (domain === hostWithoutPort) return true;
193
+ if (domain.startsWith(".")) {
194
+ const domainWithoutDot = domain.slice(1);
195
+ return hostWithoutPort.endsWith(domainWithoutDot);
196
+ }
197
+ return false;
198
+ }
188
199
  class ShokupanContext {
189
200
  constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
190
201
  this.request = request;
@@ -223,12 +234,20 @@ class ShokupanContext {
223
234
  _bodyType;
224
235
  _bodyParsed = false;
225
236
  _bodyParseError;
237
+ _routeMatched = false;
226
238
  // Cached URL properties to avoid repeated parsing
227
239
  _cachedHostname;
228
240
  _cachedProtocol;
229
241
  _cachedHost;
230
242
  _cachedOrigin;
231
243
  _cachedQuery;
244
+ /**
245
+ * JSX Rendering Function
246
+ */
247
+ renderer;
248
+ setRenderer(renderer) {
249
+ this.renderer = renderer;
250
+ }
232
251
  get url() {
233
252
  if (!this._url) {
234
253
  const urlString = this.request.url || "http://localhost/";
@@ -278,16 +297,20 @@ class ShokupanContext {
278
297
  */
279
298
  get query() {
280
299
  if (this._cachedQuery) return this._cachedQuery;
281
- const q = {};
300
+ const q = /* @__PURE__ */ Object.create(null);
301
+ const blocklist = ["__proto__", "constructor", "prototype"];
282
302
  const entries = Object.entries(this.url.searchParams);
283
303
  for (let i = 0; i < entries.length; i++) {
284
304
  const [key, value] = entries[i];
285
- if (q[key] === void 0) {
286
- q[key] = value;
287
- } else if (Array.isArray(q[key])) {
288
- q[key].push(value);
305
+ if (blocklist.includes(key)) continue;
306
+ if (Object.prototype.hasOwnProperty.call(q, key)) {
307
+ if (Array.isArray(q[key])) {
308
+ q[key].push(value);
309
+ } else {
310
+ q[key] = [q[key], value];
311
+ }
289
312
  } else {
290
- q[key] = [q[key], value];
313
+ q[key] = value;
291
314
  }
292
315
  }
293
316
  this._cachedQuery = q;
@@ -364,6 +387,12 @@ class ShokupanContext {
364
387
  * @param options Cookie options
365
388
  */
366
389
  setCookie(name, value, options = {}) {
390
+ if (options.domain) {
391
+ const currentHost = this.hostname;
392
+ if (!isValidCookieDomain(options.domain, currentHost)) {
393
+ throw new Error(`Invalid cookie domain: ${options.domain} for host ${currentHost}`);
394
+ }
395
+ }
367
396
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
368
397
  if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
369
398
  if (options.domain) cookie += `; Domain=${options.domain}`;
@@ -436,11 +465,15 @@ class ShokupanContext {
436
465
  }
437
466
  const contentType = this.request.headers.get("content-type") || "";
438
467
  if (contentType.includes("application/json") || contentType.includes("+json")) {
439
- const rawText = await this.readRawBody();
440
468
  const parserType = this.app?.applicationConfig?.jsonParser || "native";
441
469
  if (parserType === "native") {
442
- this._cachedBody = JSON.parse(rawText);
470
+ try {
471
+ this._cachedBody = await this.request.json();
472
+ } catch (e) {
473
+ throw e;
474
+ }
443
475
  } else {
476
+ const rawText = await this.request.text();
444
477
  const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
445
478
  const parser = getJSONParser(parserType);
446
479
  this._cachedBody = parser(rawText);
@@ -450,7 +483,7 @@ class ShokupanContext {
450
483
  this._cachedBody = await this.request.formData();
451
484
  this._bodyType = "formData";
452
485
  } else {
453
- this._cachedBody = await this.readRawBody();
486
+ this._cachedBody = await this.request.text();
454
487
  this._bodyType = "text";
455
488
  }
456
489
  this._bodyParsed = true;
@@ -628,10 +661,6 @@ class ShokupanContext {
628
661
  return this._finalResponse;
629
662
  }
630
663
  }
631
- /**
632
- * JSX Rendering Function
633
- */
634
- renderer;
635
664
  /**
636
665
  * Render a JSX element
637
666
  * @param element JSX Element
@@ -650,271 +679,67 @@ class ShokupanContext {
650
679
  return this.html(html, status, headers);
651
680
  }
652
681
  }
653
- function RateLimitMiddleware(options = {}) {
654
- const windowMs = options.windowMs || 60 * 1e3;
655
- const max = options.limit || options.max || 5;
656
- const message = options.message || "Too many requests, please try again later.";
657
- const statusCode = options.statusCode || 429;
658
- const headers = options.headers !== false;
659
- const mode = options.mode || "user";
660
- const keyGenerator = options.keyGenerator || ((ctx) => {
661
- if (mode === "absolute") {
662
- return "global";
663
- }
664
- return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
665
- });
666
- const skip = options.skip || (() => false);
667
- const hits = /* @__PURE__ */ new Map();
668
- const interval = setInterval(() => {
669
- const now = Date.now();
670
- const entries = Array.from(hits.entries());
671
- for (let i = 0; i < entries.length; i++) {
672
- const [key, record] = entries[i];
673
- if (record.resetTime <= now) {
674
- hits.delete(key);
682
+ const compose = (middleware) => {
683
+ if (!middleware.length) {
684
+ return (context, next) => {
685
+ return next ? next() : Promise.resolve();
686
+ };
687
+ }
688
+ return function dispatch(context, next) {
689
+ let index = -1;
690
+ async function runner(i) {
691
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
692
+ index = i;
693
+ if (i >= middleware.length) {
694
+ return next ? next() : Promise.resolve();
675
695
  }
676
- }
677
- }, windowMs);
678
- if (interval.unref) interval.unref();
679
- const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
680
- if (skip(ctx)) return next();
681
- const key = keyGenerator(ctx);
682
- const now = Date.now();
683
- let record = hits.get(key);
684
- if (!record || record.resetTime <= now) {
685
- record = {
686
- hits: 0,
687
- resetTime: now + windowMs
688
- };
689
- hits.set(key, record);
690
- }
691
- record.hits++;
692
- const remaining = Math.max(0, max - record.hits);
693
- const resetTime = Math.ceil(record.resetTime / 1e3);
694
- const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
695
- const setHeaders = (res) => {
696
- if (!headers || !res || !res.headers) return;
697
- try {
698
- res.headers.set("X-RateLimit-Limit", String(max));
699
- res.headers.set("X-RateLimit-Remaining", String(remaining));
700
- res.headers.set("X-RateLimit-Reset", String(resetTime));
701
- } catch (e) {
696
+ const fn = middleware[i];
697
+ if (!context._debug) {
698
+ return fn(context, () => runner(i + 1));
702
699
  }
703
- };
704
- if (record.hits > max) {
705
- typeof message === "object" ? JSON.stringify(message) : String(message);
706
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
707
- if (headers) {
708
- setHeaders(res);
709
- res.headers.set("Retry-After", String(retryAfter));
700
+ const debug = context._debug;
701
+ const debugId = fn._debugId || fn.name || "anonymous";
702
+ const previousNode = debug.getCurrentNode();
703
+ debug.trackEdge(previousNode, debugId);
704
+ debug.setNode(debugId);
705
+ const start = performance.now();
706
+ try {
707
+ const res = await Promise.resolve(fn(context, () => runner(i + 1)));
708
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
709
+ return res;
710
+ } catch (err) {
711
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
712
+ return Promise.reject(err);
713
+ } finally {
714
+ if (previousNode) debug.setNode(previousNode);
710
715
  }
711
- return res;
712
- }
713
- const response = await next();
714
- if (response instanceof Response && headers) {
715
- setHeaders(response);
716
716
  }
717
- return response;
717
+ return runner(0);
718
718
  };
719
- rateLimitMiddleware.isBuiltin = true;
720
- rateLimitMiddleware.pluginName = "RateLimit";
721
- return rateLimitMiddleware;
722
- }
723
- const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
724
- const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
725
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
726
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
727
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
728
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
729
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
730
- const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
731
- const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
732
- const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
733
- const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
734
- const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
735
- const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
736
- const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
737
- const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
738
- const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
739
- var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
740
- RouteParamType2["BODY"] = "BODY";
741
- RouteParamType2["PARAM"] = "PARAM";
742
- RouteParamType2["QUERY"] = "QUERY";
743
- RouteParamType2["HEADER"] = "HEADER";
744
- RouteParamType2["REQUEST"] = "REQUEST";
745
- RouteParamType2["CONTEXT"] = "CONTEXT";
746
- return RouteParamType2;
747
- })(RouteParamType || {});
748
- function Controller(path2 = "/") {
749
- return (target) => {
750
- target[$controllerPath] = path2;
751
- };
752
- }
753
- function Use(...middleware) {
754
- return (target, propertyKey, descriptor) => {
755
- if (!propertyKey) {
756
- const existing = target[$middleware] || [];
757
- target[$middleware] = [...existing, ...middleware];
758
- } else {
759
- if (!target[$middleware]) {
760
- target[$middleware] = /* @__PURE__ */ new Map();
761
- }
762
- const existing = target[$middleware].get(propertyKey) || [];
763
- target[$middleware].set(propertyKey, [...existing, ...middleware]);
764
- }
765
- };
766
- }
767
- function createParamDecorator(type) {
768
- return (name) => {
769
- return (target, propertyKey, parameterIndex) => {
770
- if (!target[$routeArgs]) {
771
- target[$routeArgs] = /* @__PURE__ */ new Map();
772
- }
773
- if (!target[$routeArgs].has(propertyKey)) {
774
- target[$routeArgs].set(propertyKey, []);
775
- }
776
- target[$routeArgs].get(propertyKey).push({
777
- index: parameterIndex,
778
- type,
779
- name
780
- });
781
- };
782
- };
783
- }
784
- const Body = createParamDecorator(RouteParamType.BODY);
785
- const Param = createParamDecorator(RouteParamType.PARAM);
786
- const Query = createParamDecorator(RouteParamType.QUERY);
787
- const Headers$1 = createParamDecorator(RouteParamType.HEADER);
788
- const Req = createParamDecorator(RouteParamType.REQUEST);
789
- const Ctx = createParamDecorator(RouteParamType.CONTEXT);
790
- function Spec(spec) {
791
- return (target, propertyKey, descriptor) => {
792
- if (!target[$routeSpec]) {
793
- target[$routeSpec] = /* @__PURE__ */ new Map();
794
- }
795
- target[$routeSpec].set(propertyKey, spec);
796
- };
797
- }
798
- function createMethodDecorator(method) {
799
- return (path2 = "/") => {
800
- return (target, propertyKey, descriptor) => {
801
- if (!target[$routeMethods]) {
802
- target[$routeMethods] = /* @__PURE__ */ new Map();
803
- }
804
- target[$routeMethods].set(propertyKey, {
805
- method,
806
- path: path2
807
- });
808
- };
809
- };
810
- }
811
- const Get = createMethodDecorator("GET");
812
- const Post = createMethodDecorator("POST");
813
- const Put = createMethodDecorator("PUT");
814
- const Delete = createMethodDecorator("DELETE");
815
- const Patch = createMethodDecorator("PATCH");
816
- const Options = createMethodDecorator("OPTIONS");
817
- const Head = createMethodDecorator("HEAD");
818
- const All = createMethodDecorator("ALL");
819
- function RateLimit(options) {
820
- return Use(RateLimitMiddleware(options));
821
- }
822
- class Container {
823
- static services = /* @__PURE__ */ new Map();
824
- static register(target, instance) {
825
- this.services.set(target, instance);
826
- }
827
- static get(target) {
828
- return this.services.get(target);
829
- }
830
- static has(target) {
831
- return this.services.has(target);
832
- }
833
- static resolve(target) {
834
- if (this.services.has(target)) {
835
- return this.services.get(target);
836
- }
837
- const instance = new target();
838
- this.services.set(target, instance);
839
- return instance;
840
- }
841
- }
842
- function Injectable() {
843
- return (target) => {
844
- };
845
- }
846
- function Inject(token) {
847
- return (target, key) => {
848
- Object.defineProperty(target, key, {
849
- get: () => Container.resolve(token),
850
- enumerable: true,
851
- configurable: true
852
- });
853
- };
854
- }
855
- const compose = (middleware) => {
856
- if (!middleware.length) {
857
- return (context, next) => {
858
- return next ? next() : Promise.resolve();
859
- };
860
- }
861
- return function dispatch(context, next) {
862
- let index = -1;
863
- async function runner(i) {
864
- if (i <= index) return Promise.reject(new Error("next() called multiple times"));
865
- index = i;
866
- if (i >= middleware.length) {
867
- return next ? next() : Promise.resolve();
868
- }
869
- const fn = middleware[i];
870
- if (!context._debug) {
871
- return fn(context, () => runner(i + 1));
719
+ };
720
+ const tracer = api.trace.getTracer("shokupan.middleware");
721
+ function traceHandler(fn, name) {
722
+ return async function(...args) {
723
+ return tracer.startActiveSpan(`route handler - ${name}`, {
724
+ kind: api.SpanKind.INTERNAL,
725
+ attributes: {
726
+ "http.route": name,
727
+ "component": "shokupan.route"
872
728
  }
873
- const debug = context._debug;
874
- const debugId = fn._debugId || fn.name || "anonymous";
875
- const previousNode = debug.getCurrentNode();
876
- debug.trackEdge(previousNode, debugId);
877
- debug.setNode(debugId);
878
- const start = performance.now();
729
+ }, async (span) => {
879
730
  try {
880
- const res = await Promise.resolve(fn(context, () => runner(i + 1)));
881
- debug.trackStep(debugId, "middleware", performance.now() - start, "success");
882
- return res;
731
+ const result = await fn.apply(this, args);
732
+ return result;
883
733
  } catch (err) {
884
- debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
885
- return Promise.reject(err);
734
+ span.recordException(err);
735
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
736
+ throw err;
886
737
  } finally {
887
- if (previousNode) debug.setNode(previousNode);
738
+ span.end();
888
739
  }
889
- }
890
- return runner(0);
740
+ });
891
741
  };
892
- };
893
- class ShokupanRequestBase {
894
- method;
895
- url;
896
- headers;
897
- body;
898
- async json() {
899
- return JSON.parse(this.body);
900
- }
901
- async text() {
902
- return this.body;
903
- }
904
- async formData() {
905
- if (this.body instanceof FormData) {
906
- return this.body;
907
- }
908
- return new Response(this.body, { headers: this.headers }).formData();
909
- }
910
- constructor(props) {
911
- Object.assign(this, props);
912
- if (!(this.headers instanceof Headers)) {
913
- this.headers = new Headers(this.headers);
914
- }
915
- }
916
742
  }
917
- const ShokupanRequest = ShokupanRequestBase;
918
743
  function isObject(item) {
919
744
  return item && typeof item === "object" && !Array.isArray(item);
920
745
  }
@@ -948,6 +773,21 @@ function deepMerge(target, ...sources) {
948
773
  }
949
774
  return deepMerge(target, ...sources);
950
775
  }
776
+ const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
777
+ const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
778
+ const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
779
+ const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
780
+ const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
781
+ const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
782
+ const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
783
+ const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
784
+ const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
785
+ const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
786
+ const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
787
+ const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
788
+ const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
789
+ const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
790
+ const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
951
791
  const REGEX_PATTERNS = {
952
792
  QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
953
793
  QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
@@ -1140,9 +980,9 @@ async function generateOpenApi(rootRouter, options = {}) {
1140
980
  const defaultTagName = options.defaultTag || "Application";
1141
981
  let astRoutes = [];
1142
982
  try {
1143
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-Bei1sVWp.cjs"));
1144
- const analyzer = new OpenAPIAnalyzer(process.cwd());
1145
- const { applications } = await analyzer.analyze();
983
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-Bei1sVWp.cjs"));
984
+ const analyzer2 = new OpenAPIAnalyzer(process.cwd());
985
+ const { applications } = await analyzer2.analyze();
1146
986
  astRoutes = await getAstRoutes(applications);
1147
987
  } catch (e) {
1148
988
  }
@@ -1329,6 +1169,11 @@ async function generateOpenApi(rootRouter, options = {}) {
1329
1169
  "x-tagGroups": xTagGroups
1330
1170
  };
1331
1171
  }
1172
+ class RequestContextStore {
1173
+ request;
1174
+ span;
1175
+ }
1176
+ const asyncContext = new node_async_hooks.AsyncLocalStorage();
1332
1177
  const eta$1 = new eta$2.Eta();
1333
1178
  function serveStatic(config, prefix) {
1334
1179
  const rootPath = path.resolve(config.root || ".");
@@ -1337,12 +1182,23 @@ function serveStatic(config, prefix) {
1337
1182
  let relative = ctx.path.slice(normalizedPrefix.length);
1338
1183
  if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
1339
1184
  if (relative.length === 0) relative = "/";
1340
- relative = decodeURIComponent(relative);
1341
- const requestPath = path.join(rootPath, relative);
1342
- if (!requestPath.startsWith(rootPath)) {
1185
+ if (relative.includes("\0")) {
1186
+ return ctx.json({ error: "Forbidden" }, 403);
1187
+ }
1188
+ try {
1189
+ relative = decodeURIComponent(relative);
1190
+ } catch (e) {
1191
+ return ctx.json({ error: "Bad Request" }, 400);
1192
+ }
1193
+ if (relative.includes("\0")) {
1343
1194
  return ctx.json({ error: "Forbidden" }, 403);
1344
1195
  }
1345
- if (requestPath.includes("\0")) {
1196
+ if (relative.includes("../") || relative.includes("..\\")) {
1197
+ return ctx.json({ error: "Forbidden" }, 403);
1198
+ }
1199
+ const requestPath = path.resolve(path.join(rootPath, relative));
1200
+ const normalizedRoot = path.resolve(rootPath);
1201
+ if (!requestPath.startsWith(normalizedRoot + path.sep) && requestPath !== normalizedRoot) {
1346
1202
  return ctx.json({ error: "Forbidden" }, 403);
1347
1203
  }
1348
1204
  if (config.hooks?.onRequest) {
@@ -1470,107 +1326,6 @@ function serveStatic(config, prefix) {
1470
1326
  serveStaticMiddleware.pluginName = "ServeStatic";
1471
1327
  return serveStaticMiddleware;
1472
1328
  }
1473
- class RouterTrie {
1474
- root;
1475
- constructor() {
1476
- this.root = this.createNode();
1477
- }
1478
- createNode() {
1479
- return {
1480
- children: {}
1481
- };
1482
- }
1483
- insert(method, path2, handler) {
1484
- let node = this.root;
1485
- const segments = this.splitPath(path2);
1486
- for (let i = 0; i < segments.length; i++) {
1487
- const segment = segments[i];
1488
- if (segment === "**") {
1489
- if (!node.recursiveChild) {
1490
- node.recursiveChild = this.createNode();
1491
- }
1492
- node = node.recursiveChild;
1493
- } else if (segment === "*") {
1494
- if (!node.wildcardChild) {
1495
- node.wildcardChild = this.createNode();
1496
- }
1497
- node = node.wildcardChild;
1498
- } else if (segment.startsWith(":")) {
1499
- const paramName = segment.slice(1);
1500
- if (!node.paramChild) {
1501
- node.paramChild = this.createNode();
1502
- node.paramChild.paramName = paramName;
1503
- }
1504
- node = node.paramChild;
1505
- node.paramName = paramName;
1506
- } else {
1507
- if (!node.children[segment]) {
1508
- node.children[segment] = this.createNode();
1509
- }
1510
- node = node.children[segment];
1511
- }
1512
- }
1513
- if (!node.handlers) {
1514
- node.handlers = {};
1515
- }
1516
- node.handlers[method] = handler;
1517
- }
1518
- search(method, path2) {
1519
- const segments = this.splitPath(path2);
1520
- const params = {};
1521
- const match = this.findNode(this.root, segments, 0, params);
1522
- if (match && match.handlers) {
1523
- const handler = match.handlers[method] || match.handlers["ALL"];
1524
- if (handler) {
1525
- return { handler, params };
1526
- }
1527
- if (method === "HEAD" && match.handlers["GET"]) {
1528
- return { handler: match.handlers["GET"], params };
1529
- }
1530
- }
1531
- return null;
1532
- }
1533
- findNode(node, segments, index, params) {
1534
- if (index === segments.length) {
1535
- if (node.handlers) return node;
1536
- if (node.recursiveChild && node.recursiveChild.handlers) {
1537
- return node.recursiveChild;
1538
- }
1539
- return null;
1540
- }
1541
- const segment = segments[index];
1542
- const child = node.children[segment];
1543
- if (child) {
1544
- const result = this.findNode(child, segments, index + 1, params);
1545
- if (result) return result;
1546
- }
1547
- if (node.paramChild) {
1548
- params[node.paramChild.paramName] = segment;
1549
- const result = this.findNode(node.paramChild, segments, index + 1, params);
1550
- if (result) return result;
1551
- delete params[node.paramChild.paramName];
1552
- }
1553
- if (node.wildcardChild) {
1554
- const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1555
- if (result) return result;
1556
- }
1557
- if (node.recursiveChild) {
1558
- const remaining = segments.length - index;
1559
- for (let k = 0; k <= remaining; k++) {
1560
- const result = this.findNode(node.recursiveChild, segments, index + k, params);
1561
- if (result) return result;
1562
- }
1563
- }
1564
- return null;
1565
- }
1566
- splitPath(path2) {
1567
- if (path2 === "/" || path2 === "") return [];
1568
- const s = path2.startsWith("/") ? path2.slice(1) : path2;
1569
- if (s === "") return [];
1570
- return s.split("/");
1571
- }
1572
- }
1573
- const asyncContext = new node_async_hooks.AsyncLocalStorage();
1574
1329
  let db;
1575
1330
  let dbPromise = null;
1576
1331
  let RecordId;
@@ -1634,29 +1389,64 @@ const datastore = {
1634
1389
  process.on("exit", async () => {
1635
1390
  if (db) await db.close();
1636
1391
  });
1637
- const tracer = api.trace.getTracer("shokupan.middleware");
1638
- function traceHandler(fn, name) {
1639
- return async function(...args) {
1640
- return tracer.startActiveSpan(`route handler - ${name}`, {
1641
- kind: api.SpanKind.INTERNAL,
1642
- attributes: {
1643
- "http.route": name,
1644
- "component": "shokupan.route"
1645
- }
1646
- }, async (span) => {
1647
- try {
1648
- const result = await fn.apply(this, args);
1649
- return result;
1650
- } catch (err) {
1651
- span.recordException(err);
1652
- span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
1653
- throw err;
1654
- } finally {
1655
- span.end();
1656
- }
1392
+ class Container {
1393
+ static services = /* @__PURE__ */ new Map();
1394
+ static register(target, instance) {
1395
+ this.services.set(target, instance);
1396
+ }
1397
+ static get(target) {
1398
+ return this.services.get(target);
1399
+ }
1400
+ static has(target) {
1401
+ return this.services.has(target);
1402
+ }
1403
+ static resolve(target) {
1404
+ if (this.services.has(target)) {
1405
+ return this.services.get(target);
1406
+ }
1407
+ const instance = new target();
1408
+ this.services.set(target, instance);
1409
+ return instance;
1410
+ }
1411
+ }
1412
+ function Injectable() {
1413
+ return (target) => {
1414
+ };
1415
+ }
1416
+ function Inject(token) {
1417
+ return (target, key) => {
1418
+ Object.defineProperty(target, key, {
1419
+ get: () => Container.resolve(token),
1420
+ enumerable: true,
1421
+ configurable: true
1657
1422
  });
1658
1423
  };
1659
1424
  }
1425
+ class ShokupanRequestBase {
1426
+ method;
1427
+ url;
1428
+ headers;
1429
+ body;
1430
+ async json() {
1431
+ return JSON.parse(this.body);
1432
+ }
1433
+ async text() {
1434
+ return this.body;
1435
+ }
1436
+ async formData() {
1437
+ if (this.body instanceof FormData) {
1438
+ return this.body;
1439
+ }
1440
+ return new Response(this.body, { headers: this.headers }).formData();
1441
+ }
1442
+ constructor(props) {
1443
+ Object.assign(this, props);
1444
+ if (!(this.headers instanceof Headers)) {
1445
+ this.headers = new Headers(this.headers);
1446
+ }
1447
+ }
1448
+ }
1449
+ const ShokupanRequest = ShokupanRequestBase;
1660
1450
  function getCallerInfo(skipFrames = 1) {
1661
1451
  let file = "unknown";
1662
1452
  let line = 0;
@@ -1682,12 +1472,120 @@ function getCallerInfo(skipFrames = 1) {
1682
1472
  }
1683
1473
  }
1684
1474
  }
1685
- } catch (e) {
1475
+ } catch (e) {
1476
+ }
1477
+ return { file, line };
1478
+ }
1479
+ class RouterTrie {
1480
+ root;
1481
+ constructor() {
1482
+ this.root = this.createNode();
1483
+ }
1484
+ createNode() {
1485
+ return {
1486
+ children: {}
1487
+ };
1488
+ }
1489
+ insert(method, path2, handler) {
1490
+ let node = this.root;
1491
+ const segments = this.splitPath(path2);
1492
+ for (let i = 0; i < segments.length; i++) {
1493
+ const segment = segments[i];
1494
+ if (segment === "**") {
1495
+ if (!node.recursiveChild) {
1496
+ node.recursiveChild = this.createNode();
1497
+ }
1498
+ node = node.recursiveChild;
1499
+ } else if (segment === "*") {
1500
+ if (!node.wildcardChild) {
1501
+ node.wildcardChild = this.createNode();
1502
+ }
1503
+ node = node.wildcardChild;
1504
+ } else if (segment.startsWith(":")) {
1505
+ const paramName = segment.slice(1);
1506
+ if (!node.paramChild) {
1507
+ node.paramChild = this.createNode();
1508
+ node.paramChild.paramName = paramName;
1509
+ }
1510
+ node = node.paramChild;
1511
+ node.paramName = paramName;
1512
+ } else {
1513
+ if (!node.children[segment]) {
1514
+ node.children[segment] = this.createNode();
1515
+ }
1516
+ node = node.children[segment];
1517
+ }
1518
+ }
1519
+ if (!node.handlers) {
1520
+ node.handlers = {};
1521
+ }
1522
+ node.handlers[method] = handler;
1523
+ }
1524
+ search(method, path2) {
1525
+ const segments = this.splitPath(path2);
1526
+ const params = {};
1527
+ const match = this.findNode(this.root, segments, 0, params);
1528
+ if (match && match.handlers) {
1529
+ const handler = match.handlers[method] || match.handlers["ALL"];
1530
+ if (handler) {
1531
+ return { handler, params };
1532
+ }
1533
+ if (method === "HEAD" && match.handlers["GET"]) {
1534
+ return { handler: match.handlers["GET"], params };
1535
+ }
1536
+ }
1537
+ return null;
1538
+ }
1539
+ findNode(node, segments, index, params) {
1540
+ if (index === segments.length) {
1541
+ if (node.handlers) return node;
1542
+ if (node.recursiveChild && node.recursiveChild.handlers) {
1543
+ return node.recursiveChild;
1544
+ }
1545
+ return null;
1546
+ }
1547
+ const segment = segments[index];
1548
+ const child = node.children[segment];
1549
+ if (child) {
1550
+ const result = this.findNode(child, segments, index + 1, params);
1551
+ if (result) return result;
1552
+ }
1553
+ if (node.paramChild) {
1554
+ params[node.paramChild.paramName] = segment;
1555
+ const result = this.findNode(node.paramChild, segments, index + 1, params);
1556
+ if (result) return result;
1557
+ delete params[node.paramChild.paramName];
1558
+ }
1559
+ if (node.wildcardChild) {
1560
+ const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1561
+ if (result) return result;
1562
+ }
1563
+ if (node.recursiveChild) {
1564
+ const remaining = segments.length - index;
1565
+ for (let k = 0; k <= remaining; k++) {
1566
+ const result = this.findNode(node.recursiveChild, segments, index + k, params);
1567
+ if (result) return result;
1568
+ }
1569
+ }
1570
+ return null;
1571
+ }
1572
+ splitPath(path2) {
1573
+ if (path2 === "/" || path2 === "") return [];
1574
+ const s = path2.startsWith("/") ? path2.slice(1) : path2;
1575
+ if (s === "") return [];
1576
+ return s.split("/");
1686
1577
  }
1687
- return { file, line };
1688
1578
  }
1689
- const RouterRegistry = /* @__PURE__ */ new Map();
1690
- const ShokupanApplicationTree = {};
1579
+ const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1580
+ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1581
+ RouteParamType2["BODY"] = "BODY";
1582
+ RouteParamType2["PARAM"] = "PARAM";
1583
+ RouteParamType2["QUERY"] = "QUERY";
1584
+ RouteParamType2["HEADER"] = "HEADER";
1585
+ RouteParamType2["REQUEST"] = "REQUEST";
1586
+ RouteParamType2["CONTEXT"] = "CONTEXT";
1587
+ return RouteParamType2;
1588
+ })(RouteParamType || {});
1691
1589
  class ShokupanRouter {
1692
1590
  constructor(config) {
1693
1591
  this.config = config;
@@ -1798,216 +1696,9 @@ class ShokupanRouter {
1798
1696
  throw new Error(`[Shokupan] strict controller check failed: ${controller.constructor.name || typeof controller} is not a class constructor.`);
1799
1697
  }
1800
1698
  if (this.isRouterInstance(controller)) {
1801
- if (controller[$isMounted]) {
1802
- throw new Error("Router is already mounted");
1803
- }
1804
- controller[$mountPath] = prefix;
1805
- if (!controller.metadata) {
1806
- const info = getCallerInfo();
1807
- controller.metadata = {
1808
- file: info.file,
1809
- line: info.line,
1810
- name: "MountedRouter"
1811
- };
1812
- }
1813
- this[$childRouters].push(controller);
1814
- controller[$parent] = this;
1815
- const setRouterContext = (router) => {
1816
- router[$appRoot] = this.root;
1817
- router[$childRouters].forEach((child) => setRouterContext(child));
1818
- };
1819
- setRouterContext(controller);
1820
- if (this[$appRoot]) ;
1821
- controller[$appRoot] = this.root;
1822
- controller[$isMounted] = true;
1699
+ this.mountRouter(prefix, controller);
1823
1700
  } else {
1824
- let instance = controller;
1825
- if (typeof controller === "function") {
1826
- instance = Container.resolve(controller);
1827
- const controllerPath = controller[$controllerPath];
1828
- if (controllerPath) {
1829
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1830
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1831
- prefix = p1 + p2;
1832
- if (!prefix) prefix = "/";
1833
- }
1834
- } else {
1835
- const ctor = instance.constructor;
1836
- const controllerPath = ctor[$controllerPath];
1837
- if (controllerPath) {
1838
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1839
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1840
- prefix = p1 + p2;
1841
- if (!prefix) prefix = "/";
1842
- }
1843
- }
1844
- instance[$mountPath] = prefix;
1845
- const info = getCallerInfo();
1846
- instance.metadata = {
1847
- file: info.file,
1848
- line: info.line,
1849
- name: instance.constructor.name
1850
- };
1851
- this[$childControllers].push(instance);
1852
- const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1853
- const proto = Object.getPrototypeOf(instance);
1854
- const methods = /* @__PURE__ */ new Set();
1855
- let current = proto;
1856
- while (current && current !== Object.prototype) {
1857
- Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1858
- current = Object.getPrototypeOf(current);
1859
- }
1860
- Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1861
- const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1862
- const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1863
- const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1864
- let routesAttached = 0;
1865
- for (let i = 0; i < Array.from(methods).length; i++) {
1866
- const name = Array.from(methods)[i];
1867
- if (name === "constructor") continue;
1868
- if (["arguments", "caller", "callee"].includes(name)) continue;
1869
- const originalHandler = instance[name];
1870
- if (typeof originalHandler !== "function") continue;
1871
- let method;
1872
- let subPath = "";
1873
- if (decoratedRoutes && decoratedRoutes.has(name)) {
1874
- const config = decoratedRoutes.get(name);
1875
- method = config.method;
1876
- subPath = config.path;
1877
- } else {
1878
- for (let j = 0; j < HTTPMethods.length; j++) {
1879
- const m = HTTPMethods[j];
1880
- if (name.toUpperCase().startsWith(m)) {
1881
- method = m;
1882
- const rest = name.slice(m.length);
1883
- if (rest.length === 0) {
1884
- subPath = "/";
1885
- } else {
1886
- subPath = "";
1887
- let buffer = "";
1888
- const flush = () => {
1889
- if (buffer.length > 0) {
1890
- subPath += "/" + buffer.toLowerCase();
1891
- buffer = "";
1892
- }
1893
- };
1894
- for (let i2 = 0; i2 < rest.length; i2++) {
1895
- const char = rest[i2];
1896
- if (char === "$") {
1897
- flush();
1898
- subPath += "/:";
1899
- continue;
1900
- }
1901
- }
1902
- subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1903
- if (!subPath.startsWith("/")) {
1904
- subPath = "/" + subPath;
1905
- }
1906
- }
1907
- break;
1908
- }
1909
- }
1910
- }
1911
- if (method) {
1912
- routesAttached++;
1913
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1914
- const cleanSubPath = subPath === "/" ? "" : subPath;
1915
- let joined;
1916
- if (cleanSubPath.length === 0) {
1917
- joined = cleanPrefix;
1918
- } else if (cleanSubPath.startsWith("/")) {
1919
- joined = cleanPrefix + cleanSubPath;
1920
- } else {
1921
- joined = cleanPrefix + "/" + cleanSubPath;
1922
- }
1923
- const fullPath = joined || "/";
1924
- const normalizedPath = fullPath.replace(/\/+/g, "/");
1925
- const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1926
- const allMiddleware = [...controllerMiddleware, ...methodMw];
1927
- const routeArgs = decoratedArgs && decoratedArgs.get(name);
1928
- const wrappedHandler = async (ctx) => {
1929
- let args = [ctx];
1930
- if (routeArgs?.length > 0) {
1931
- args = [];
1932
- const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1933
- for (let k = 0; k < sortedArgs.length; k++) {
1934
- const arg = sortedArgs[k];
1935
- switch (arg.type) {
1936
- case RouteParamType.BODY:
1937
- try {
1938
- if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1939
- args[arg.index] = await ctx.req.json();
1940
- } else {
1941
- const text = await ctx.req.text();
1942
- if (!text) {
1943
- args[arg.index] = {};
1944
- } else {
1945
- args[arg.index] = JSON.parse(text);
1946
- }
1947
- }
1948
- } catch (e) {
1949
- const err = new Error("Invalid JSON body");
1950
- err.status = 400;
1951
- throw err;
1952
- }
1953
- break;
1954
- case RouteParamType.PARAM:
1955
- args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1956
- break;
1957
- case RouteParamType.QUERY: {
1958
- const url = new URL(ctx.req.url);
1959
- if (arg.name) {
1960
- const vals = url.searchParams.getAll(arg.name);
1961
- args[arg.index] = vals.length > 1 ? vals : vals[0];
1962
- } else {
1963
- const query = {};
1964
- const keys = Object.keys(url.searchParams);
1965
- for (let k2 = 0; k2 < keys.length; k2++) {
1966
- const key = keys[k2];
1967
- const vals = url.searchParams.getAll(key);
1968
- query[key] = vals.length > 1 ? vals : vals[0];
1969
- }
1970
- args[arg.index] = query;
1971
- }
1972
- break;
1973
- }
1974
- case RouteParamType.HEADER:
1975
- args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
1976
- break;
1977
- case RouteParamType.REQUEST:
1978
- args[arg.index] = ctx.req;
1979
- break;
1980
- case RouteParamType.CONTEXT:
1981
- args[arg.index] = ctx;
1982
- break;
1983
- }
1984
- }
1985
- }
1986
- const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1987
- return tracedOriginalHandler.apply(instance, args);
1988
- };
1989
- let finalHandler = wrappedHandler;
1990
- if (allMiddleware.length > 0) {
1991
- const composed = compose(allMiddleware);
1992
- finalHandler = async (ctx) => {
1993
- return composed(ctx, () => wrappedHandler(ctx));
1994
- };
1995
- }
1996
- finalHandler.originalHandler = originalHandler;
1997
- if (finalHandler !== wrappedHandler) {
1998
- wrappedHandler.originalHandler = originalHandler;
1999
- }
2000
- const tagName = instance.constructor.name;
2001
- const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2002
- const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2003
- const spec = { tags: [tagName], ...userSpec };
2004
- this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2005
- }
2006
- }
2007
- if (routesAttached === 0) {
2008
- console.warn(`No routes attached to controller ${instance.constructor.name}`);
2009
- }
2010
- instance[$isMounted] = true;
1701
+ this.scanControllerRoutes(prefix, controller);
2011
1702
  }
2012
1703
  return this;
2013
1704
  }
@@ -2045,8 +1736,6 @@ class ShokupanRouter {
2045
1736
  */
2046
1737
  async internalRequest(arg) {
2047
1738
  const options = typeof arg === "string" ? { path: arg } : arg;
2048
- const store = asyncContext.getStore();
2049
- store?.get("req");
2050
1739
  let url = options.path;
2051
1740
  if (!url.startsWith("http")) {
2052
1741
  const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig.port || 3e3}`;
@@ -2158,6 +1847,220 @@ class ShokupanRouter {
2158
1847
  wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
2159
1848
  return wrapped;
2160
1849
  }
1850
+ mountRouter(prefix, router) {
1851
+ if (router[$isMounted]) {
1852
+ throw new Error("Router is already mounted");
1853
+ }
1854
+ router[$mountPath] = prefix;
1855
+ if (!router.metadata) {
1856
+ const info = getCallerInfo();
1857
+ router.metadata = {
1858
+ file: info.file,
1859
+ line: info.line,
1860
+ name: "MountedRouter"
1861
+ };
1862
+ }
1863
+ this[$childRouters].push(router);
1864
+ router[$parent] = this;
1865
+ const setRouterContext = (router2) => {
1866
+ router2[$appRoot] = this.root;
1867
+ router2[$childRouters].forEach((child) => setRouterContext(child));
1868
+ };
1869
+ setRouterContext(router);
1870
+ router[$appRoot] = this.root;
1871
+ router[$isMounted] = true;
1872
+ }
1873
+ scanControllerRoutes(prefix, controller) {
1874
+ let instance = controller;
1875
+ if (typeof controller === "function") {
1876
+ instance = Container.resolve(controller);
1877
+ const controllerPath = controller[$controllerPath];
1878
+ if (controllerPath) {
1879
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1880
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1881
+ prefix = p1 + p2;
1882
+ if (!prefix) prefix = "/";
1883
+ }
1884
+ } else {
1885
+ const ctor = instance.constructor;
1886
+ const controllerPath = ctor[$controllerPath];
1887
+ if (controllerPath) {
1888
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1889
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1890
+ prefix = p1 + p2;
1891
+ if (!prefix) prefix = "/";
1892
+ }
1893
+ }
1894
+ instance[$mountPath] = prefix;
1895
+ const info = getCallerInfo();
1896
+ instance.metadata = {
1897
+ file: info.file,
1898
+ line: info.line,
1899
+ name: instance.constructor.name
1900
+ };
1901
+ this[$childControllers].push(instance);
1902
+ const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1903
+ const proto = Object.getPrototypeOf(instance);
1904
+ const methods = /* @__PURE__ */ new Set();
1905
+ let current = proto;
1906
+ while (current && current !== Object.prototype) {
1907
+ Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1908
+ current = Object.getPrototypeOf(current);
1909
+ }
1910
+ Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1911
+ const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1912
+ const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1913
+ const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1914
+ let routesAttached = 0;
1915
+ for (let i = 0; i < Array.from(methods).length; i++) {
1916
+ const name = Array.from(methods)[i];
1917
+ if (name === "constructor") continue;
1918
+ if (["arguments", "caller", "callee"].includes(name)) continue;
1919
+ const originalHandler = instance[name];
1920
+ if (typeof originalHandler !== "function") continue;
1921
+ let method;
1922
+ let subPath = "";
1923
+ if (decoratedRoutes && decoratedRoutes.has(name)) {
1924
+ const config = decoratedRoutes.get(name);
1925
+ method = config.method;
1926
+ subPath = config.path;
1927
+ } else {
1928
+ for (let j = 0; j < HTTPMethods.length; j++) {
1929
+ const m = HTTPMethods[j];
1930
+ if (name.toUpperCase().startsWith(m)) {
1931
+ method = m;
1932
+ const rest = name.slice(m.length);
1933
+ if (rest.length === 0) {
1934
+ subPath = "/";
1935
+ } else {
1936
+ subPath = "";
1937
+ let buffer = "";
1938
+ const flush = () => {
1939
+ if (buffer.length > 0) {
1940
+ subPath += "/" + buffer.toLowerCase();
1941
+ buffer = "";
1942
+ }
1943
+ };
1944
+ for (let i2 = 0; i2 < rest.length; i2++) {
1945
+ const char = rest[i2];
1946
+ if (char === "$") {
1947
+ flush();
1948
+ subPath += "/:";
1949
+ continue;
1950
+ }
1951
+ buffer += char;
1952
+ }
1953
+ if (buffer.length > 0) flush();
1954
+ subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1955
+ if (!subPath.startsWith("/")) {
1956
+ subPath = "/" + subPath;
1957
+ }
1958
+ }
1959
+ break;
1960
+ }
1961
+ }
1962
+ }
1963
+ if (method) {
1964
+ routesAttached++;
1965
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1966
+ const cleanSubPath = subPath === "/" ? "" : subPath;
1967
+ let joined;
1968
+ if (cleanSubPath.length === 0) {
1969
+ joined = cleanPrefix;
1970
+ } else if (cleanSubPath.startsWith("/")) {
1971
+ joined = cleanPrefix + cleanSubPath;
1972
+ } else {
1973
+ joined = cleanPrefix + "/" + cleanSubPath;
1974
+ }
1975
+ const fullPath = joined || "/";
1976
+ const normalizedPath = fullPath.replace(/\/+/g, "/");
1977
+ const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1978
+ const allMiddleware = [...controllerMiddleware, ...methodMw];
1979
+ const routeArgs = decoratedArgs && decoratedArgs.get(name);
1980
+ const wrappedHandler = async (ctx) => {
1981
+ let args = [ctx];
1982
+ if (routeArgs?.length > 0) {
1983
+ args = [];
1984
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1985
+ for (let k = 0; k < sortedArgs.length; k++) {
1986
+ const arg = sortedArgs[k];
1987
+ switch (arg.type) {
1988
+ case RouteParamType.BODY:
1989
+ try {
1990
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1991
+ args[arg.index] = await ctx.req.json();
1992
+ } else {
1993
+ const text = await ctx.req.text();
1994
+ if (!text) {
1995
+ args[arg.index] = {};
1996
+ } else {
1997
+ args[arg.index] = JSON.parse(text);
1998
+ }
1999
+ }
2000
+ } catch (e) {
2001
+ const err = new Error("Invalid JSON body");
2002
+ err.status = 400;
2003
+ throw err;
2004
+ }
2005
+ break;
2006
+ case RouteParamType.PARAM:
2007
+ args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
2008
+ break;
2009
+ case RouteParamType.QUERY: {
2010
+ const url = new URL(ctx.req.url);
2011
+ if (arg.name) {
2012
+ const vals = url.searchParams.getAll(arg.name);
2013
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
2014
+ } else {
2015
+ const query = {};
2016
+ const keys = Object.keys(url.searchParams);
2017
+ for (let k2 = 0; k2 < keys.length; k2++) {
2018
+ const key = keys[k2];
2019
+ const vals = url.searchParams.getAll(key);
2020
+ query[key] = vals.length > 1 ? vals : vals[0];
2021
+ }
2022
+ args[arg.index] = query;
2023
+ }
2024
+ break;
2025
+ }
2026
+ case RouteParamType.HEADER:
2027
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2028
+ break;
2029
+ case RouteParamType.REQUEST:
2030
+ args[arg.index] = ctx.req;
2031
+ break;
2032
+ case RouteParamType.CONTEXT:
2033
+ args[arg.index] = ctx;
2034
+ break;
2035
+ }
2036
+ }
2037
+ }
2038
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
2039
+ return tracedOriginalHandler.apply(instance, args);
2040
+ };
2041
+ let finalHandler = wrappedHandler;
2042
+ if (allMiddleware.length > 0) {
2043
+ const composed = compose(allMiddleware);
2044
+ finalHandler = async (ctx) => {
2045
+ return composed(ctx, () => wrappedHandler(ctx));
2046
+ };
2047
+ }
2048
+ finalHandler.originalHandler = originalHandler;
2049
+ if (finalHandler !== wrappedHandler) {
2050
+ wrappedHandler.originalHandler = originalHandler;
2051
+ }
2052
+ const tagName = instance.constructor.name;
2053
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2054
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2055
+ const spec = { tags: [tagName], ...userSpec };
2056
+ this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2057
+ }
2058
+ }
2059
+ if (routesAttached === 0) {
2060
+ console.warn(`No routes attached to controller ${instance.constructor.name}`);
2061
+ }
2062
+ instance[$isMounted] = true;
2063
+ }
2161
2064
  /**
2162
2065
  * Find a route matching the given method and path.
2163
2066
  * @param method HTTP method
@@ -2191,10 +2094,13 @@ class ShokupanRouter {
2191
2094
  }
2192
2095
  parsePath(path2) {
2193
2096
  const keys = [];
2097
+ if (path2.length > 2048) {
2098
+ throw new Error("Path too long");
2099
+ }
2194
2100
  const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
2195
2101
  keys.push(key);
2196
- return "([^/]+)";
2197
- }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
2102
+ return "([^/]{1,255})";
2103
+ }).replace(/\*\*/g, ".{0,1000}").replace(/\*/g, "[^/]{1,255}");
2198
2104
  return {
2199
2105
  regex: new RegExp(`^${pattern}$`),
2200
2106
  keys
@@ -2281,7 +2187,7 @@ class ShokupanRouter {
2281
2187
  if (effectiveRenderer) {
2282
2188
  const innerHandler = wrappedHandler;
2283
2189
  wrappedHandler = async (ctx) => {
2284
- ctx.renderer = effectiveRenderer;
2190
+ ctx.setRenderer(effectiveRenderer);
2285
2191
  return innerHandler(ctx);
2286
2192
  };
2287
2193
  }
@@ -2683,6 +2589,13 @@ class Shokupan extends ShokupanRouter {
2683
2589
  }
2684
2590
  return this;
2685
2591
  }
2592
+ /**
2593
+ * Registers a plugin.
2594
+ */
2595
+ register(plugin, options) {
2596
+ plugin.onInit(this, options);
2597
+ return this;
2598
+ }
2686
2599
  startupHooks = [];
2687
2600
  /**
2688
2601
  * Registers a callback to be executed before the server starts listening.
@@ -2745,7 +2658,7 @@ class Shokupan extends ShokupanRouter {
2745
2658
  };
2746
2659
  let factory = this.applicationConfig.serverFactory;
2747
2660
  if (!factory && typeof Bun === "undefined") {
2748
- const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-DFhwlK8e.cjs"));
2661
+ const { createHttpServer } = await Promise.resolve().then(() => require("./http-server-DFhwlK8e.cjs"));
2749
2662
  factory = createHttpServer();
2750
2663
  }
2751
2664
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2814,19 +2727,19 @@ class Shokupan extends ShokupanRouter {
2814
2727
  "http.method": req.method
2815
2728
  }
2816
2729
  };
2817
- const parent = store?.get("span");
2730
+ const parent = store?.span;
2818
2731
  const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
2819
2732
  return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2820
- const ctxMap = /* @__PURE__ */ new Map();
2821
- ctxMap.set("span", span);
2822
- ctxMap.set("request", req);
2823
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
2733
+ const ctxStore = new RequestContextStore();
2734
+ ctxStore.span = span;
2735
+ ctxStore.request = req;
2736
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server).finally(() => span.end()));
2824
2737
  });
2825
2738
  }
2826
2739
  if (this.applicationConfig.enableAsyncLocalStorage) {
2827
- const ctxMap = /* @__PURE__ */ new Map();
2828
- ctxMap.set("request", req);
2829
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
2740
+ const ctxStore = new RequestContextStore();
2741
+ ctxStore.request = req;
2742
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server));
2830
2743
  }
2831
2744
  return this.handleRequest(req, server);
2832
2745
  }
@@ -2848,6 +2761,7 @@ class Shokupan extends ShokupanRouter {
2848
2761
  const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2849
2762
  const match = this.find(req.method, ctx.path);
2850
2763
  if (match) {
2764
+ ctx._routeMatched = true;
2851
2765
  ctx.params = match.params;
2852
2766
  await bodyParsing;
2853
2767
  return match.handler(ctx);
@@ -2862,10 +2776,14 @@ class Shokupan extends ShokupanRouter {
2862
2776
  } else if (result === null || result === void 0) {
2863
2777
  if (ctx._finalResponse instanceof Response) {
2864
2778
  response = ctx._finalResponse;
2865
- } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
2779
+ } else if (ctx._routeMatched) {
2866
2780
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2867
2781
  } else {
2868
- response = ctx.text("Not Found", 404);
2782
+ if (ctx.response.status !== 200) {
2783
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2784
+ } else {
2785
+ response = ctx.text("Not Found", 404);
2786
+ }
2869
2787
  }
2870
2788
  } else if (typeof result === "object") {
2871
2789
  response = ctx.json(result);
@@ -2877,7 +2795,7 @@ class Shokupan extends ShokupanRouter {
2877
2795
  return response;
2878
2796
  } catch (err) {
2879
2797
  console.error(err);
2880
- const span = asyncContext.getStore()?.get("span");
2798
+ const span = asyncContext.getStore()?.span;
2881
2799
  if (span) span.setStatus({ code: 2 });
2882
2800
  const status = err.status || err.statusCode || 500;
2883
2801
  const body = { error: err.message || "Internal Server Error" };
@@ -2885,31 +2803,195 @@ class Shokupan extends ShokupanRouter {
2885
2803
  await this.runHooks("onError", ctx, err);
2886
2804
  return ctx.json(body, status);
2887
2805
  }
2888
- };
2889
- let executionPromise = handle();
2890
- const timeoutMs = this.applicationConfig.requestTimeout;
2891
- if (timeoutMs && timeoutMs > 0) {
2892
- let timeoutId;
2893
- const timeoutPromise = new Promise((_, reject) => {
2894
- timeoutId = setTimeout(async () => {
2895
- controller.abort();
2896
- await this.runHooks("onRequestTimeout", ctx);
2897
- reject(new Error("Request Timeout"));
2898
- }, timeoutMs);
2806
+ };
2807
+ let executionPromise = handle();
2808
+ const timeoutMs = this.applicationConfig.requestTimeout;
2809
+ if (timeoutMs && timeoutMs > 0) {
2810
+ let timeoutId;
2811
+ const timeoutPromise = new Promise((_, reject) => {
2812
+ timeoutId = setTimeout(async () => {
2813
+ controller.abort();
2814
+ await this.runHooks("onRequestTimeout", ctx);
2815
+ reject(new Error("Request Timeout"));
2816
+ }, timeoutMs);
2817
+ });
2818
+ executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2819
+ }
2820
+ return executionPromise.catch((err) => {
2821
+ if (err.message === "Request Timeout") {
2822
+ return ctx.text("Request Timeout", 408);
2823
+ }
2824
+ console.error("Unexpected error in request execution:", err);
2825
+ return ctx.text("Internal Server Error", 500);
2826
+ }).then(async (res) => {
2827
+ await this.runHooks("onResponseEnd", ctx, res);
2828
+ return res;
2829
+ });
2830
+ }
2831
+ }
2832
+ function RateLimitMiddleware(options = {}) {
2833
+ const windowMs = options.windowMs || 60 * 1e3;
2834
+ const max = options.limit || options.max || 5;
2835
+ const message = options.message || "Too many requests, please try again later.";
2836
+ const statusCode = options.statusCode || 429;
2837
+ const headers = options.headers !== false;
2838
+ const mode = options.mode || "user";
2839
+ const trustedProxies = options.trustedProxies || [];
2840
+ const keyGenerator = options.keyGenerator || ((ctx) => {
2841
+ if (mode === "absolute") {
2842
+ return "global";
2843
+ }
2844
+ const xForwardedFor = ctx.headers.get("x-forwarded-for");
2845
+ if (xForwardedFor && trustedProxies.length > 0) {
2846
+ const ips = xForwardedFor.split(",").map((ip) => ip.trim());
2847
+ for (let i = ips.length - 1; i >= 0; i--) {
2848
+ const ip = ips[i];
2849
+ if (!trustedProxies.includes(ip)) {
2850
+ if (/^[\d.:a-fA-F]+$/.test(ip)) {
2851
+ return ip;
2852
+ }
2853
+ }
2854
+ }
2855
+ }
2856
+ return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
2857
+ });
2858
+ const skip = options.skip || (() => false);
2859
+ const hits = /* @__PURE__ */ new Map();
2860
+ const interval = setInterval(() => {
2861
+ const now = Date.now();
2862
+ const entries = Array.from(hits.entries());
2863
+ for (let i = 0; i < entries.length; i++) {
2864
+ const [key, record] = entries[i];
2865
+ if (record.resetTime <= now) {
2866
+ hits.delete(key);
2867
+ }
2868
+ }
2869
+ }, windowMs);
2870
+ if (interval.unref) interval.unref();
2871
+ const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
2872
+ if (skip(ctx)) return next();
2873
+ const key = keyGenerator(ctx);
2874
+ const now = Date.now();
2875
+ let record = hits.get(key);
2876
+ if (!record || record.resetTime <= now) {
2877
+ record = {
2878
+ hits: 0,
2879
+ resetTime: now + windowMs
2880
+ };
2881
+ hits.set(key, record);
2882
+ }
2883
+ record.hits++;
2884
+ const remaining = Math.max(0, max - record.hits);
2885
+ const resetTime = Math.ceil(record.resetTime / 1e3);
2886
+ const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
2887
+ const setHeaders = (res) => {
2888
+ if (!headers || !res || !res.headers) return;
2889
+ try {
2890
+ res.headers.set("X-RateLimit-Limit", String(max));
2891
+ res.headers.set("X-RateLimit-Remaining", String(remaining));
2892
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
2893
+ } catch (e) {
2894
+ }
2895
+ };
2896
+ if (record.hits > max) {
2897
+ if (options.onRateLimited) {
2898
+ const result = await options.onRateLimited(ctx, key);
2899
+ if (result instanceof Response) {
2900
+ return result;
2901
+ }
2902
+ }
2903
+ const msg = typeof message === "function" ? message(ctx, key) : message;
2904
+ typeof msg === "object" ? JSON.stringify(msg) : String(msg);
2905
+ const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
2906
+ if (headers) {
2907
+ setHeaders(res);
2908
+ res.headers.set("Retry-After", String(retryAfter));
2909
+ }
2910
+ return res;
2911
+ }
2912
+ const response = await next();
2913
+ if (response instanceof Response && headers) {
2914
+ setHeaders(response);
2915
+ }
2916
+ return response;
2917
+ };
2918
+ rateLimitMiddleware.isBuiltin = true;
2919
+ rateLimitMiddleware.pluginName = "RateLimit";
2920
+ return rateLimitMiddleware;
2921
+ }
2922
+ function Controller(path2 = "/") {
2923
+ return (target) => {
2924
+ target[$controllerPath] = path2;
2925
+ };
2926
+ }
2927
+ function Use(...middleware) {
2928
+ return (target, propertyKey, descriptor) => {
2929
+ if (!propertyKey) {
2930
+ const existing = target[$middleware] || [];
2931
+ target[$middleware] = [...existing, ...middleware];
2932
+ } else {
2933
+ if (!target[$middleware]) {
2934
+ target[$middleware] = /* @__PURE__ */ new Map();
2935
+ }
2936
+ const existing = target[$middleware].get(propertyKey) || [];
2937
+ target[$middleware].set(propertyKey, [...existing, ...middleware]);
2938
+ }
2939
+ };
2940
+ }
2941
+ function createParamDecorator(type) {
2942
+ return (name) => {
2943
+ return (target, propertyKey, parameterIndex) => {
2944
+ if (!target[$routeArgs]) {
2945
+ target[$routeArgs] = /* @__PURE__ */ new Map();
2946
+ }
2947
+ if (!target[$routeArgs].has(propertyKey)) {
2948
+ target[$routeArgs].set(propertyKey, []);
2949
+ }
2950
+ target[$routeArgs].get(propertyKey).push({
2951
+ index: parameterIndex,
2952
+ type,
2953
+ name
2899
2954
  });
2900
- executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2955
+ };
2956
+ };
2957
+ }
2958
+ const Body = createParamDecorator(RouteParamType.BODY);
2959
+ const Param = createParamDecorator(RouteParamType.PARAM);
2960
+ const Query = createParamDecorator(RouteParamType.QUERY);
2961
+ const Headers$1 = createParamDecorator(RouteParamType.HEADER);
2962
+ const Req = createParamDecorator(RouteParamType.REQUEST);
2963
+ const Ctx = createParamDecorator(RouteParamType.CONTEXT);
2964
+ function Spec(spec) {
2965
+ return (target, propertyKey, descriptor) => {
2966
+ if (!target[$routeSpec]) {
2967
+ target[$routeSpec] = /* @__PURE__ */ new Map();
2901
2968
  }
2902
- return executionPromise.catch((err) => {
2903
- if (err.message === "Request Timeout") {
2904
- return ctx.text("Request Timeout", 408);
2969
+ target[$routeSpec].set(propertyKey, spec);
2970
+ };
2971
+ }
2972
+ function createMethodDecorator(method) {
2973
+ return (path2 = "/") => {
2974
+ return (target, propertyKey, descriptor) => {
2975
+ if (!target[$routeMethods]) {
2976
+ target[$routeMethods] = /* @__PURE__ */ new Map();
2905
2977
  }
2906
- console.error("Unexpected error in request execution:", err);
2907
- return ctx.text("Internal Server Error", 500);
2908
- }).then(async (res) => {
2909
- await this.runHooks("onResponseEnd", ctx, res);
2910
- return res;
2911
- });
2912
- }
2978
+ target[$routeMethods].set(propertyKey, {
2979
+ method,
2980
+ path: path2
2981
+ });
2982
+ };
2983
+ };
2984
+ }
2985
+ const Get = createMethodDecorator("GET");
2986
+ const Post = createMethodDecorator("POST");
2987
+ const Put = createMethodDecorator("PUT");
2988
+ const Delete = createMethodDecorator("DELETE");
2989
+ const Patch = createMethodDecorator("PATCH");
2990
+ const Options = createMethodDecorator("OPTIONS");
2991
+ const Head = createMethodDecorator("HEAD");
2992
+ const All = createMethodDecorator("ALL");
2993
+ function RateLimit(options) {
2994
+ return Use(RateLimitMiddleware(options));
2913
2995
  }
2914
2996
  class AuthPlugin extends ShokupanRouter {
2915
2997
  constructor(authConfig) {
@@ -2919,6 +3001,13 @@ class AuthPlugin extends ShokupanRouter {
2919
3001
  this.init();
2920
3002
  }
2921
3003
  secret;
3004
+ onInit(app, options) {
3005
+ if (options?.path) {
3006
+ app.mount(options.path, this);
3007
+ } else {
3008
+ app.mount(options.path ?? "/", this);
3009
+ }
3010
+ }
2922
3011
  getProviderInstance(name, p) {
2923
3012
  switch (name) {
2924
3013
  case "github":
@@ -2982,9 +3071,10 @@ class AuthPlugin extends ShokupanRouter {
2982
3071
  } else {
2983
3072
  return ctx.text("Provider config error", 500);
2984
3073
  }
2985
- ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; Max-Age=600`);
3074
+ const isSecure = ctx.secure;
3075
+ ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
2986
3076
  if (codeVerifier) {
2987
- ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; Max-Age=600`);
3077
+ ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
2988
3078
  }
2989
3079
  return ctx.redirect(url.toString());
2990
3080
  });
@@ -3025,7 +3115,7 @@ class AuthPlugin extends ShokupanRouter {
3025
3115
  return ctx.json({ token: jwt, user });
3026
3116
  } catch (e) {
3027
3117
  console.error("Auth Error", e);
3028
- return ctx.text("Authentication failed: " + e.message + "\n" + e.stack, 500);
3118
+ return ctx.text("Authentication failed. Please try again.", 500);
3029
3119
  }
3030
3120
  });
3031
3121
  }
@@ -3135,8 +3225,196 @@ class AuthPlugin extends ShokupanRouter {
3135
3225
  };
3136
3226
  }
3137
3227
  }
3228
+ class ClusterPlugin {
3229
+ constructor(options = {}) {
3230
+ this.options = options;
3231
+ }
3232
+ onInit(app) {
3233
+ const originalListen = app.listen.bind(app);
3234
+ const { workers = "auto", silent = false, sticky = false } = this.options;
3235
+ const isBun = typeof Bun !== "undefined";
3236
+ const numCPUs = os.cpus().length;
3237
+ const numWorkers = workers === "auto" || workers === -1 ? numCPUs : workers;
3238
+ if (numWorkers <= 1) {
3239
+ return;
3240
+ }
3241
+ app.listen = async (port) => {
3242
+ const finalPort = port ?? app.applicationConfig.port ?? 3e3;
3243
+ if (isBun) {
3244
+ return this.handleBun(app, finalPort, numWorkers, originalListen);
3245
+ } else {
3246
+ return this.handleNode(app, finalPort, numWorkers, originalListen, silent, sticky);
3247
+ }
3248
+ };
3249
+ }
3250
+ async handleBun(app, port, workers, originalListen) {
3251
+ const workerId = process.env["SHOKUPAN_WORKER_ID"];
3252
+ if (workerId) {
3253
+ app.applicationConfig.reusePort = true;
3254
+ return originalListen(port);
3255
+ }
3256
+ console.log(`[Cluster] Starting ${workers} Bun workers on port ${port}...`);
3257
+ const spawnWorker = (id) => {
3258
+ Bun.spawn([process.argv0, ...process.argv.slice(1)], {
3259
+ env: { ...process.env, SHOKUPAN_WORKER_ID: id },
3260
+ stdio: ["inherit", "inherit", "inherit"],
3261
+ onExit(proc, exitCode, signalCode, error) {
3262
+ console.log(`[Cluster] Worker ${id} died (code: ${exitCode}). Restarting...`);
3263
+ spawnWorker(id);
3264
+ }
3265
+ });
3266
+ };
3267
+ for (let i = 0; i < workers; i++) {
3268
+ spawnWorker(process.pid + "_" + i + 1);
3269
+ }
3270
+ setInterval(() => {
3271
+ }, 1e3 * 60 * 60);
3272
+ return {
3273
+ stop: () => {
3274
+ },
3275
+ port
3276
+ };
3277
+ }
3278
+ async handleNode(app, port, workers, originalListen, silent, sticky) {
3279
+ if (cluster.isPrimary) {
3280
+ console.log(`[Cluster] Master ${process.pid} is running`);
3281
+ const fork = () => cluster.fork(process.env);
3282
+ for (let i = 0; i < workers; i++) {
3283
+ fork();
3284
+ }
3285
+ cluster.on("exit", (worker, code, signal) => {
3286
+ console.log(`[Cluster] Worker ${worker.process.pid} died. Restarting...`);
3287
+ fork();
3288
+ });
3289
+ if (sticky) {
3290
+ const server = net.createServer({ pauseOnConnect: true }, (connection) => {
3291
+ const remote = connection.remoteAddress || "";
3292
+ let hash = 0;
3293
+ for (let i = 0; i < remote.length; i++) {
3294
+ hash = (hash << 5) - hash + remote.charCodeAt(i);
3295
+ hash |= 0;
3296
+ }
3297
+ const index = Math.abs(hash) % workers;
3298
+ const worker = Object.values(cluster.workers)[index];
3299
+ if (worker) {
3300
+ worker.send("sticky-session:connection", connection);
3301
+ } else {
3302
+ connection.end();
3303
+ }
3304
+ });
3305
+ server.listen(port, () => {
3306
+ console.log(`[Cluster] Sticky Load Balancer listening on port ${port}`);
3307
+ });
3308
+ return {
3309
+ close: () => server.close(),
3310
+ port
3311
+ };
3312
+ } else {
3313
+ return {
3314
+ close: () => {
3315
+ },
3316
+ // Master controls
3317
+ port
3318
+ };
3319
+ }
3320
+ } else {
3321
+ if (sticky) {
3322
+ const server = await originalListen(0);
3323
+ process.on("message", (message, handle) => {
3324
+ if (message !== "sticky-session:connection") return;
3325
+ if (!handle) return;
3326
+ server.emit("connection", handle);
3327
+ handle.resume();
3328
+ });
3329
+ return server;
3330
+ } else {
3331
+ return originalListen(port);
3332
+ }
3333
+ }
3334
+ }
3335
+ }
3336
+ const eta = new eta$2.Eta();
3337
+ class ScalarPlugin extends ShokupanRouter {
3338
+ constructor(pluginOptions = {}) {
3339
+ pluginOptions.config ??= {};
3340
+ super();
3341
+ this.pluginOptions = pluginOptions;
3342
+ this.init();
3343
+ }
3344
+ onInit(app, options) {
3345
+ if (options?.path) {
3346
+ app.mount(options.path, this);
3347
+ } else {
3348
+ app.mount(options.path ?? "/", this);
3349
+ }
3350
+ this.onMount(app);
3351
+ }
3352
+ init() {
3353
+ this.get("/", (ctx) => {
3354
+ let path2 = ctx.url.toString();
3355
+ if (!path2.endsWith("/")) path2 += "/";
3356
+ return ctx.html(eta.renderString(`<!doctype html>
3357
+ <html>
3358
+ <head>
3359
+ <title>API Reference</title>
3360
+ <meta charset = "utf-8" />
3361
+ <meta name="viewport" content = "width=device-width, initial-scale=1" />
3362
+ </head>
3363
+
3364
+ <body>
3365
+ <div id="app"></div>
3366
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
3367
+ <script>
3368
+ Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3369
+ url: "<%= it.path %>openapi.json",
3370
+ }
3371
+ ])
3372
+ <\/script>
3373
+ </body>
3374
+
3375
+ </html>`, { path: path2, config: this.pluginOptions }));
3376
+ });
3377
+ this.get("/openapi.json", async (ctx) => {
3378
+ let spec;
3379
+ if (this.root.openApiSpec) {
3380
+ try {
3381
+ spec = structuredClone(this.root.openApiSpec);
3382
+ } catch (e) {
3383
+ spec = Object.assign({}, this.root.openApiSpec);
3384
+ }
3385
+ } else {
3386
+ spec = await (this.root || this).generateApiSpec();
3387
+ }
3388
+ if (this.pluginOptions.baseDocument) {
3389
+ deepMerge(spec, this.pluginOptions.baseDocument);
3390
+ }
3391
+ return ctx.json(spec);
3392
+ });
3393
+ }
3394
+ // New lifecycle method to be called by router.mount
3395
+ onMount(parent) {
3396
+ if (parent.onStart) {
3397
+ parent.onStart(async () => {
3398
+ if (this.pluginOptions.enableStaticAnalysis) {
3399
+ try {
3400
+ const entrypoint = process.argv[1];
3401
+ console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3402
+ const analyzer$1 = new analyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
3403
+ let staticSpec = await analyzer$1.analyze();
3404
+ if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
3405
+ deepMerge(this.pluginOptions.baseDocument, staticSpec);
3406
+ console.log("[ScalarPlugin] Static analysis completed successfully.");
3407
+ } catch (err) {
3408
+ console.error("[ScalarPlugin] Failed to run static analysis:", err);
3409
+ }
3410
+ }
3411
+ });
3412
+ }
3413
+ }
3414
+ }
3138
3415
  function Compression(options = {}) {
3139
3416
  const threshold = options.threshold ?? 512;
3417
+ const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
3140
3418
  const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
3141
3419
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
3142
3420
  let method = null;
@@ -3149,6 +3427,9 @@ function Compression(options = {}) {
3149
3427
  } else if (acceptEncoding.includes("gzip")) method = "gzip";
3150
3428
  else if (acceptEncoding.includes("deflate")) method = "deflate";
3151
3429
  if (!method) return next();
3430
+ if (!allowedAlgorithms.has(method)) {
3431
+ return next();
3432
+ }
3152
3433
  let response = await next();
3153
3434
  if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
3154
3435
  response = ctx._finalResponse;
@@ -3236,14 +3517,21 @@ function Cors(options = {}) {
3236
3517
  const origin = ctx.headers.get("origin");
3237
3518
  const set = (k, v) => headers.set(k, v);
3238
3519
  const append = (k, v) => headers.append(k, v);
3520
+ if (origin === "null" && opts.origin !== "null") {
3521
+ return next();
3522
+ }
3239
3523
  if (opts.origin === "*") {
3240
3524
  set("Access-Control-Allow-Origin", "*");
3241
3525
  } else if (typeof opts.origin === "string") {
3242
3526
  set("Access-Control-Allow-Origin", opts.origin);
3243
3527
  } else if (Array.isArray(opts.origin)) {
3244
- if (origin && opts.origin.includes(origin)) {
3245
- set("Access-Control-Allow-Origin", origin);
3246
- append("Vary", "Origin");
3528
+ if (origin) {
3529
+ const normalizedOrigin = origin.toLowerCase();
3530
+ const normalizedAllowed = opts.origin.map((o) => o.toLowerCase());
3531
+ if (normalizedAllowed.includes(normalizedOrigin)) {
3532
+ set("Access-Control-Allow-Origin", origin);
3533
+ append("Vary", "Origin");
3534
+ }
3247
3535
  }
3248
3536
  } else if (typeof opts.origin === "function") {
3249
3537
  const allowed = opts.origin(ctx);
@@ -3696,77 +3984,6 @@ function enableOpenApiValidation(app) {
3696
3984
  precompileValidators(app, spec);
3697
3985
  });
3698
3986
  }
3699
- const eta = new eta$2.Eta();
3700
- class ScalarPlugin extends ShokupanRouter {
3701
- constructor(pluginOptions = {}) {
3702
- pluginOptions.config ??= {};
3703
- super();
3704
- this.pluginOptions = pluginOptions;
3705
- this.init();
3706
- }
3707
- init() {
3708
- this.get("/", (ctx) => {
3709
- let path2 = ctx.url.toString();
3710
- if (!path2.endsWith("/")) path2 += "/";
3711
- return ctx.html(eta.renderString(`<!doctype html>
3712
- <html>
3713
- <head>
3714
- <title>API Reference</title>
3715
- <meta charset = "utf-8" />
3716
- <meta name="viewport" content = "width=device-width, initial-scale=1" />
3717
- </head>
3718
-
3719
- <body>
3720
- <div id="app"></div>
3721
- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
3722
- <script>
3723
- Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3724
- url: "<%= it.path %>openapi.json",
3725
- }
3726
- ])
3727
- <\/script>
3728
- </body>
3729
-
3730
- </html>`, { path: path2, config: this.pluginOptions }));
3731
- });
3732
- this.get("/openapi.json", async (ctx) => {
3733
- let spec;
3734
- if (this.root.openApiSpec) {
3735
- try {
3736
- spec = structuredClone(this.root.openApiSpec);
3737
- } catch (e) {
3738
- spec = Object.assign({}, this.root.openApiSpec);
3739
- }
3740
- } else {
3741
- spec = await (this.root || this).generateApiSpec();
3742
- }
3743
- if (this.pluginOptions.baseDocument) {
3744
- deepMerge(spec, this.pluginOptions.baseDocument);
3745
- }
3746
- return ctx.json(spec);
3747
- });
3748
- }
3749
- // New lifecycle method to be called by router.mount
3750
- onMount(parent) {
3751
- if (parent.onStart) {
3752
- parent.onStart(async () => {
3753
- if (this.pluginOptions.enableStaticAnalysis) {
3754
- try {
3755
- const entrypoint = process.argv[1];
3756
- console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3757
- const analyzer = new openapiAnalyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
3758
- let staticSpec = await analyzer.analyze();
3759
- if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
3760
- deepMerge(this.pluginOptions.baseDocument, staticSpec);
3761
- console.log("[ScalarPlugin] Static analysis completed successfully.");
3762
- } catch (err) {
3763
- console.error("[ScalarPlugin] Failed to run static analysis:", err);
3764
- }
3765
- }
3766
- });
3767
- }
3768
- }
3769
- }
3770
3987
  function SecurityHeaders(options = {}) {
3771
3988
  const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
3772
3989
  const headers = {};
@@ -3890,18 +4107,18 @@ class MemoryStore extends events.EventEmitter {
3890
4107
  }
3891
4108
  set(sid, sess, cb) {
3892
4109
  this.sessions[sid] = JSON.stringify(sess);
3893
- cb && cb();
4110
+ cb?.();
3894
4111
  }
3895
4112
  destroy(sid, cb) {
3896
4113
  delete this.sessions[sid];
3897
- cb && cb();
4114
+ cb?.();
3898
4115
  }
3899
4116
  touch(sid, sess, cb) {
3900
4117
  const current = this.sessions[sid];
3901
4118
  if (current) {
3902
4119
  this.sessions[sid] = JSON.stringify(sess);
3903
4120
  }
3904
- cb && cb();
4121
+ cb?.();
3905
4122
  }
3906
4123
  all(cb) {
3907
4124
  const result = {};
@@ -3917,7 +4134,7 @@ class MemoryStore extends events.EventEmitter {
3917
4134
  }
3918
4135
  clear(cb) {
3919
4136
  this.sessions = {};
3920
- cb && cb();
4137
+ cb?.();
3921
4138
  }
3922
4139
  }
3923
4140
  function sign(val, secret) {
@@ -3930,11 +4147,17 @@ function unsign(input, secret) {
3930
4147
  if (typeof secret !== "string") throw new TypeError("Secret string must be provided.");
3931
4148
  const tentValue = input.slice(0, input.lastIndexOf("."));
3932
4149
  const expectedInput = sign(tentValue, secret);
3933
- const expectedBuffer = Buffer.from(expectedInput);
3934
- const inputBuffer = Buffer.from(input);
3935
- if (expectedBuffer.length !== inputBuffer.length) return false;
3936
- const valid = require("crypto").timingSafeEqual(expectedBuffer, inputBuffer);
3937
- return valid ? tentValue : false;
4150
+ const maxLength = Math.max(expectedInput.length, input.length);
4151
+ const paddedExpected = Buffer.alloc(maxLength);
4152
+ const paddedInput = Buffer.alloc(maxLength);
4153
+ Buffer.from(expectedInput).copy(paddedExpected);
4154
+ Buffer.from(input).copy(paddedInput);
4155
+ try {
4156
+ const valid = require("crypto").timingSafeEqual(paddedExpected, paddedInput);
4157
+ return valid ? tentValue : false;
4158
+ } catch {
4159
+ return false;
4160
+ }
3938
4161
  }
3939
4162
  function Session(options) {
3940
4163
  const store = options.store || new MemoryStore();
@@ -4106,6 +4329,7 @@ exports.$routes = $routes;
4106
4329
  exports.All = All;
4107
4330
  exports.AuthPlugin = AuthPlugin;
4108
4331
  exports.Body = Body;
4332
+ exports.ClusterPlugin = ClusterPlugin;
4109
4333
  exports.Compression = Compression;
4110
4334
  exports.Container = Container;
4111
4335
  exports.Controller = Controller;
@@ -4129,16 +4353,13 @@ exports.RateLimit = RateLimit;
4129
4353
  exports.RateLimitMiddleware = RateLimitMiddleware;
4130
4354
  exports.Req = Req;
4131
4355
  exports.RouteParamType = RouteParamType;
4132
- exports.RouterRegistry = RouterRegistry;
4133
4356
  exports.ScalarPlugin = ScalarPlugin;
4134
4357
  exports.SecurityHeaders = SecurityHeaders;
4135
4358
  exports.Session = Session;
4136
4359
  exports.Shokupan = Shokupan;
4137
- exports.ShokupanApplicationTree = ShokupanApplicationTree;
4138
4360
  exports.ShokupanContext = ShokupanContext;
4139
4361
  exports.ShokupanRequest = ShokupanRequest;
4140
4362
  exports.ShokupanResponse = ShokupanResponse;
4141
- exports.ShokupanRouter = ShokupanRouter;
4142
4363
  exports.Spec = Spec;
4143
4364
  exports.Use = Use;
4144
4365
  exports.ValidationError = ValidationError;