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.js CHANGED
@@ -1,81 +1,21 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
2
4
  import { Eta } from "eta";
3
5
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
4
- import { resolve, join, basename } from "path";
5
- import { AsyncLocalStorage } from "node:async_hooks";
6
- import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
6
+ import { resolve, join, sep, basename } from "path";
7
7
  import * as os from "node:os";
8
+ import os__default from "node:os";
8
9
  import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
9
10
  import * as jose from "jose";
11
+ import cluster from "node:cluster";
12
+ import net from "node:net";
13
+ import { OpenAPIAnalyzer } from "./analyzer-Ce_7JxZh.js";
10
14
  import * as zlib from "node:zlib";
11
15
  import Ajv from "ajv";
12
16
  import addFormats from "ajv-formats";
13
- import { OpenAPIAnalyzer } from "./openapi-analyzer-Ce_7JxZh.js";
14
17
  import { randomUUID, createHmac } from "crypto";
15
18
  import { EventEmitter } from "events";
16
- class ShokupanResponse {
17
- _headers = null;
18
- _status = 200;
19
- /**
20
- * Get the current headers
21
- */
22
- get headers() {
23
- if (!this._headers) this._headers = new Headers();
24
- return this._headers;
25
- }
26
- /**
27
- * Get the current status code
28
- */
29
- get status() {
30
- return this._status;
31
- }
32
- /**
33
- * Set the status code
34
- */
35
- set status(code) {
36
- this._status = code;
37
- }
38
- /**
39
- * Set a response header
40
- * @param key Header name
41
- * @param value Header value
42
- */
43
- set(key, value) {
44
- if (!this._headers) this._headers = new Headers();
45
- this._headers.set(key, value);
46
- return this;
47
- }
48
- /**
49
- * Append to a response header
50
- * @param key Header name
51
- * @param value Header value
52
- */
53
- append(key, value) {
54
- if (!this._headers) this._headers = new Headers();
55
- this._headers.append(key, value);
56
- return this;
57
- }
58
- /**
59
- * Get a response header value
60
- * @param key Header name
61
- */
62
- get(key) {
63
- return this._headers?.get(key) || null;
64
- }
65
- /**
66
- * Check if a header exists
67
- * @param key Header name
68
- */
69
- has(key) {
70
- return this._headers?.has(key) || false;
71
- }
72
- /**
73
- * Internal: check if headers have been initialized/modified
74
- */
75
- get hasPopulatedHeaders() {
76
- return this._headers !== null;
77
- }
78
- }
79
19
  const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
80
20
  100,
81
21
  101,
@@ -142,6 +82,78 @@ const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
142
82
  511
143
83
  ]);
144
84
  const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
85
+ class ShokupanResponse {
86
+ _headers = null;
87
+ _status = 200;
88
+ /**
89
+ * Get the current headers
90
+ */
91
+ get headers() {
92
+ if (!this._headers) this._headers = new Headers();
93
+ return this._headers;
94
+ }
95
+ /**
96
+ * Get the current status code
97
+ */
98
+ get status() {
99
+ return this._status;
100
+ }
101
+ /**
102
+ * Set the status code
103
+ */
104
+ set status(code) {
105
+ this._status = code;
106
+ }
107
+ /**
108
+ * Set a response header
109
+ * @param key Header name
110
+ * @param value Header value
111
+ */
112
+ set(key, value) {
113
+ if (!this._headers) this._headers = new Headers();
114
+ this._headers.set(key, value);
115
+ return this;
116
+ }
117
+ /**
118
+ * Append to a response header
119
+ * @param key Header name
120
+ * @param value Header value
121
+ */
122
+ append(key, value) {
123
+ if (!this._headers) this._headers = new Headers();
124
+ this._headers.append(key, value);
125
+ return this;
126
+ }
127
+ /**
128
+ * Get a response header value
129
+ * @param key Header name
130
+ */
131
+ get(key) {
132
+ return this._headers?.get(key) || null;
133
+ }
134
+ /**
135
+ * Check if a header exists
136
+ * @param key Header name
137
+ */
138
+ has(key) {
139
+ return this._headers?.has(key) || false;
140
+ }
141
+ /**
142
+ * Internal: check if headers have been initialized/modified
143
+ */
144
+ get hasPopulatedHeaders() {
145
+ return this._headers !== null;
146
+ }
147
+ }
148
+ function isValidCookieDomain(domain, currentHost) {
149
+ const hostWithoutPort = currentHost.split(":")[0];
150
+ if (domain === hostWithoutPort) return true;
151
+ if (domain.startsWith(".")) {
152
+ const domainWithoutDot = domain.slice(1);
153
+ return hostWithoutPort.endsWith(domainWithoutDot);
154
+ }
155
+ return false;
156
+ }
145
157
  class ShokupanContext {
146
158
  constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
147
159
  this.request = request;
@@ -180,12 +192,20 @@ class ShokupanContext {
180
192
  _bodyType;
181
193
  _bodyParsed = false;
182
194
  _bodyParseError;
195
+ _routeMatched = false;
183
196
  // Cached URL properties to avoid repeated parsing
184
197
  _cachedHostname;
185
198
  _cachedProtocol;
186
199
  _cachedHost;
187
200
  _cachedOrigin;
188
201
  _cachedQuery;
202
+ /**
203
+ * JSX Rendering Function
204
+ */
205
+ renderer;
206
+ setRenderer(renderer) {
207
+ this.renderer = renderer;
208
+ }
189
209
  get url() {
190
210
  if (!this._url) {
191
211
  const urlString = this.request.url || "http://localhost/";
@@ -235,16 +255,20 @@ class ShokupanContext {
235
255
  */
236
256
  get query() {
237
257
  if (this._cachedQuery) return this._cachedQuery;
238
- const q = {};
258
+ const q = /* @__PURE__ */ Object.create(null);
259
+ const blocklist = ["__proto__", "constructor", "prototype"];
239
260
  const entries = Object.entries(this.url.searchParams);
240
261
  for (let i = 0; i < entries.length; i++) {
241
262
  const [key, value] = entries[i];
242
- if (q[key] === void 0) {
243
- q[key] = value;
244
- } else if (Array.isArray(q[key])) {
245
- q[key].push(value);
263
+ if (blocklist.includes(key)) continue;
264
+ if (Object.prototype.hasOwnProperty.call(q, key)) {
265
+ if (Array.isArray(q[key])) {
266
+ q[key].push(value);
267
+ } else {
268
+ q[key] = [q[key], value];
269
+ }
246
270
  } else {
247
- q[key] = [q[key], value];
271
+ q[key] = value;
248
272
  }
249
273
  }
250
274
  this._cachedQuery = q;
@@ -321,6 +345,12 @@ class ShokupanContext {
321
345
  * @param options Cookie options
322
346
  */
323
347
  setCookie(name, value, options = {}) {
348
+ if (options.domain) {
349
+ const currentHost = this.hostname;
350
+ if (!isValidCookieDomain(options.domain, currentHost)) {
351
+ throw new Error(`Invalid cookie domain: ${options.domain} for host ${currentHost}`);
352
+ }
353
+ }
324
354
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
325
355
  if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
326
356
  if (options.domain) cookie += `; Domain=${options.domain}`;
@@ -393,11 +423,15 @@ class ShokupanContext {
393
423
  }
394
424
  const contentType = this.request.headers.get("content-type") || "";
395
425
  if (contentType.includes("application/json") || contentType.includes("+json")) {
396
- const rawText = await this.readRawBody();
397
426
  const parserType = this.app?.applicationConfig?.jsonParser || "native";
398
427
  if (parserType === "native") {
399
- this._cachedBody = JSON.parse(rawText);
428
+ try {
429
+ this._cachedBody = await this.request.json();
430
+ } catch (e) {
431
+ throw e;
432
+ }
400
433
  } else {
434
+ const rawText = await this.request.text();
401
435
  const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
402
436
  const parser = getJSONParser(parserType);
403
437
  this._cachedBody = parser(rawText);
@@ -407,7 +441,7 @@ class ShokupanContext {
407
441
  this._cachedBody = await this.request.formData();
408
442
  this._bodyType = "formData";
409
443
  } else {
410
- this._cachedBody = await this.readRawBody();
444
+ this._cachedBody = await this.request.text();
411
445
  this._bodyType = "text";
412
446
  }
413
447
  this._bodyParsed = true;
@@ -585,10 +619,6 @@ class ShokupanContext {
585
619
  return this._finalResponse;
586
620
  }
587
621
  }
588
- /**
589
- * JSX Rendering Function
590
- */
591
- renderer;
592
622
  /**
593
623
  * Render a JSX element
594
624
  * @param element JSX Element
@@ -607,271 +637,67 @@ class ShokupanContext {
607
637
  return this.html(html, status, headers);
608
638
  }
609
639
  }
610
- function RateLimitMiddleware(options = {}) {
611
- const windowMs = options.windowMs || 60 * 1e3;
612
- const max = options.limit || options.max || 5;
613
- const message = options.message || "Too many requests, please try again later.";
614
- const statusCode = options.statusCode || 429;
615
- const headers = options.headers !== false;
616
- const mode = options.mode || "user";
617
- const keyGenerator = options.keyGenerator || ((ctx) => {
618
- if (mode === "absolute") {
619
- return "global";
620
- }
621
- return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
622
- });
623
- const skip = options.skip || (() => false);
624
- const hits = /* @__PURE__ */ new Map();
625
- const interval = setInterval(() => {
626
- const now = Date.now();
627
- const entries = Array.from(hits.entries());
628
- for (let i = 0; i < entries.length; i++) {
629
- const [key, record] = entries[i];
630
- if (record.resetTime <= now) {
631
- hits.delete(key);
640
+ const compose = (middleware) => {
641
+ if (!middleware.length) {
642
+ return (context2, next) => {
643
+ return next ? next() : Promise.resolve();
644
+ };
645
+ }
646
+ return function dispatch(context2, next) {
647
+ let index = -1;
648
+ async function runner(i) {
649
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
650
+ index = i;
651
+ if (i >= middleware.length) {
652
+ return next ? next() : Promise.resolve();
632
653
  }
633
- }
634
- }, windowMs);
635
- if (interval.unref) interval.unref();
636
- const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
637
- if (skip(ctx)) return next();
638
- const key = keyGenerator(ctx);
639
- const now = Date.now();
640
- let record = hits.get(key);
641
- if (!record || record.resetTime <= now) {
642
- record = {
643
- hits: 0,
644
- resetTime: now + windowMs
645
- };
646
- hits.set(key, record);
647
- }
648
- record.hits++;
649
- const remaining = Math.max(0, max - record.hits);
650
- const resetTime = Math.ceil(record.resetTime / 1e3);
651
- const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
652
- const setHeaders = (res) => {
653
- if (!headers || !res || !res.headers) return;
654
- try {
655
- res.headers.set("X-RateLimit-Limit", String(max));
656
- res.headers.set("X-RateLimit-Remaining", String(remaining));
657
- res.headers.set("X-RateLimit-Reset", String(resetTime));
658
- } catch (e) {
654
+ const fn = middleware[i];
655
+ if (!context2._debug) {
656
+ return fn(context2, () => runner(i + 1));
659
657
  }
660
- };
661
- if (record.hits > max) {
662
- typeof message === "object" ? JSON.stringify(message) : String(message);
663
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
664
- if (headers) {
665
- setHeaders(res);
666
- res.headers.set("Retry-After", String(retryAfter));
658
+ const debug = context2._debug;
659
+ const debugId = fn._debugId || fn.name || "anonymous";
660
+ const previousNode = debug.getCurrentNode();
661
+ debug.trackEdge(previousNode, debugId);
662
+ debug.setNode(debugId);
663
+ const start = performance.now();
664
+ try {
665
+ const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
666
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
667
+ return res;
668
+ } catch (err) {
669
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
670
+ return Promise.reject(err);
671
+ } finally {
672
+ if (previousNode) debug.setNode(previousNode);
667
673
  }
668
- return res;
669
- }
670
- const response = await next();
671
- if (response instanceof Response && headers) {
672
- setHeaders(response);
673
674
  }
674
- return response;
675
+ return runner(0);
675
676
  };
676
- rateLimitMiddleware.isBuiltin = true;
677
- rateLimitMiddleware.pluginName = "RateLimit";
678
- return rateLimitMiddleware;
679
- }
680
- const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
681
- const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
682
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
683
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
684
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
685
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
686
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
687
- const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
688
- const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
689
- const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
690
- const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
691
- const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
692
- const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
693
- const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
694
- const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
695
- const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
696
- var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
697
- RouteParamType2["BODY"] = "BODY";
698
- RouteParamType2["PARAM"] = "PARAM";
699
- RouteParamType2["QUERY"] = "QUERY";
700
- RouteParamType2["HEADER"] = "HEADER";
701
- RouteParamType2["REQUEST"] = "REQUEST";
702
- RouteParamType2["CONTEXT"] = "CONTEXT";
703
- return RouteParamType2;
704
- })(RouteParamType || {});
705
- function Controller(path = "/") {
706
- return (target) => {
707
- target[$controllerPath] = path;
708
- };
709
- }
710
- function Use(...middleware) {
711
- return (target, propertyKey, descriptor) => {
712
- if (!propertyKey) {
713
- const existing = target[$middleware] || [];
714
- target[$middleware] = [...existing, ...middleware];
715
- } else {
716
- if (!target[$middleware]) {
717
- target[$middleware] = /* @__PURE__ */ new Map();
718
- }
719
- const existing = target[$middleware].get(propertyKey) || [];
720
- target[$middleware].set(propertyKey, [...existing, ...middleware]);
721
- }
722
- };
723
- }
724
- function createParamDecorator(type) {
725
- return (name) => {
726
- return (target, propertyKey, parameterIndex) => {
727
- if (!target[$routeArgs]) {
728
- target[$routeArgs] = /* @__PURE__ */ new Map();
729
- }
730
- if (!target[$routeArgs].has(propertyKey)) {
731
- target[$routeArgs].set(propertyKey, []);
732
- }
733
- target[$routeArgs].get(propertyKey).push({
734
- index: parameterIndex,
735
- type,
736
- name
737
- });
738
- };
739
- };
740
- }
741
- const Body = createParamDecorator(RouteParamType.BODY);
742
- const Param = createParamDecorator(RouteParamType.PARAM);
743
- const Query = createParamDecorator(RouteParamType.QUERY);
744
- const Headers$1 = createParamDecorator(RouteParamType.HEADER);
745
- const Req = createParamDecorator(RouteParamType.REQUEST);
746
- const Ctx = createParamDecorator(RouteParamType.CONTEXT);
747
- function Spec(spec) {
748
- return (target, propertyKey, descriptor) => {
749
- if (!target[$routeSpec]) {
750
- target[$routeSpec] = /* @__PURE__ */ new Map();
751
- }
752
- target[$routeSpec].set(propertyKey, spec);
753
- };
754
- }
755
- function createMethodDecorator(method) {
756
- return (path = "/") => {
757
- return (target, propertyKey, descriptor) => {
758
- if (!target[$routeMethods]) {
759
- target[$routeMethods] = /* @__PURE__ */ new Map();
760
- }
761
- target[$routeMethods].set(propertyKey, {
762
- method,
763
- path
764
- });
765
- };
766
- };
767
- }
768
- const Get = createMethodDecorator("GET");
769
- const Post = createMethodDecorator("POST");
770
- const Put = createMethodDecorator("PUT");
771
- const Delete = createMethodDecorator("DELETE");
772
- const Patch = createMethodDecorator("PATCH");
773
- const Options = createMethodDecorator("OPTIONS");
774
- const Head = createMethodDecorator("HEAD");
775
- const All = createMethodDecorator("ALL");
776
- function RateLimit(options) {
777
- return Use(RateLimitMiddleware(options));
778
- }
779
- class Container {
780
- static services = /* @__PURE__ */ new Map();
781
- static register(target, instance) {
782
- this.services.set(target, instance);
783
- }
784
- static get(target) {
785
- return this.services.get(target);
786
- }
787
- static has(target) {
788
- return this.services.has(target);
789
- }
790
- static resolve(target) {
791
- if (this.services.has(target)) {
792
- return this.services.get(target);
793
- }
794
- const instance = new target();
795
- this.services.set(target, instance);
796
- return instance;
797
- }
798
- }
799
- function Injectable() {
800
- return (target) => {
801
- };
802
- }
803
- function Inject(token) {
804
- return (target, key) => {
805
- Object.defineProperty(target, key, {
806
- get: () => Container.resolve(token),
807
- enumerable: true,
808
- configurable: true
809
- });
810
- };
811
- }
812
- const compose = (middleware) => {
813
- if (!middleware.length) {
814
- return (context2, next) => {
815
- return next ? next() : Promise.resolve();
816
- };
817
- }
818
- return function dispatch(context2, next) {
819
- let index = -1;
820
- async function runner(i) {
821
- if (i <= index) return Promise.reject(new Error("next() called multiple times"));
822
- index = i;
823
- if (i >= middleware.length) {
824
- return next ? next() : Promise.resolve();
825
- }
826
- const fn = middleware[i];
827
- if (!context2._debug) {
828
- return fn(context2, () => runner(i + 1));
677
+ };
678
+ const tracer = trace.getTracer("shokupan.middleware");
679
+ function traceHandler(fn, name) {
680
+ return async function(...args) {
681
+ return tracer.startActiveSpan(`route handler - ${name}`, {
682
+ kind: SpanKind.INTERNAL,
683
+ attributes: {
684
+ "http.route": name,
685
+ "component": "shokupan.route"
829
686
  }
830
- const debug = context2._debug;
831
- const debugId = fn._debugId || fn.name || "anonymous";
832
- const previousNode = debug.getCurrentNode();
833
- debug.trackEdge(previousNode, debugId);
834
- debug.setNode(debugId);
835
- const start = performance.now();
687
+ }, async (span) => {
836
688
  try {
837
- const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
838
- debug.trackStep(debugId, "middleware", performance.now() - start, "success");
839
- return res;
689
+ const result = await fn.apply(this, args);
690
+ return result;
840
691
  } catch (err) {
841
- debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
842
- return Promise.reject(err);
692
+ span.recordException(err);
693
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
694
+ throw err;
843
695
  } finally {
844
- if (previousNode) debug.setNode(previousNode);
696
+ span.end();
845
697
  }
846
- }
847
- return runner(0);
698
+ });
848
699
  };
849
- };
850
- class ShokupanRequestBase {
851
- method;
852
- url;
853
- headers;
854
- body;
855
- async json() {
856
- return JSON.parse(this.body);
857
- }
858
- async text() {
859
- return this.body;
860
- }
861
- async formData() {
862
- if (this.body instanceof FormData) {
863
- return this.body;
864
- }
865
- return new Response(this.body, { headers: this.headers }).formData();
866
- }
867
- constructor(props) {
868
- Object.assign(this, props);
869
- if (!(this.headers instanceof Headers)) {
870
- this.headers = new Headers(this.headers);
871
- }
872
- }
873
700
  }
874
- const ShokupanRequest = ShokupanRequestBase;
875
701
  function isObject(item) {
876
702
  return item && typeof item === "object" && !Array.isArray(item);
877
703
  }
@@ -905,6 +731,21 @@ function deepMerge(target, ...sources) {
905
731
  }
906
732
  return deepMerge(target, ...sources);
907
733
  }
734
+ const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
735
+ const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
736
+ const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
737
+ const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
738
+ const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
739
+ const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
740
+ const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
741
+ const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
742
+ const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
743
+ const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
744
+ const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
745
+ const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
746
+ const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
747
+ const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
748
+ const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
908
749
  const REGEX_PATTERNS = {
909
750
  QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
910
751
  QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
@@ -1097,7 +938,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1097
938
  const defaultTagName = options.defaultTag || "Application";
1098
939
  let astRoutes = [];
1099
940
  try {
1100
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-Ce_7JxZh.js");
941
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-Ce_7JxZh.js");
1101
942
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
1102
943
  const { applications } = await analyzer.analyze();
1103
944
  astRoutes = await getAstRoutes(applications);
@@ -1286,6 +1127,11 @@ async function generateOpenApi(rootRouter, options = {}) {
1286
1127
  "x-tagGroups": xTagGroups
1287
1128
  };
1288
1129
  }
1130
+ class RequestContextStore {
1131
+ request;
1132
+ span;
1133
+ }
1134
+ const asyncContext = new AsyncLocalStorage();
1289
1135
  const eta$1 = new Eta();
1290
1136
  function serveStatic(config, prefix) {
1291
1137
  const rootPath = resolve(config.root || ".");
@@ -1294,12 +1140,23 @@ function serveStatic(config, prefix) {
1294
1140
  let relative = ctx.path.slice(normalizedPrefix.length);
1295
1141
  if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
1296
1142
  if (relative.length === 0) relative = "/";
1297
- relative = decodeURIComponent(relative);
1298
- const requestPath = join(rootPath, relative);
1299
- if (!requestPath.startsWith(rootPath)) {
1143
+ if (relative.includes("\0")) {
1144
+ return ctx.json({ error: "Forbidden" }, 403);
1145
+ }
1146
+ try {
1147
+ relative = decodeURIComponent(relative);
1148
+ } catch (e) {
1149
+ return ctx.json({ error: "Bad Request" }, 400);
1150
+ }
1151
+ if (relative.includes("\0")) {
1152
+ return ctx.json({ error: "Forbidden" }, 403);
1153
+ }
1154
+ if (relative.includes("../") || relative.includes("..\\")) {
1300
1155
  return ctx.json({ error: "Forbidden" }, 403);
1301
1156
  }
1302
- if (requestPath.includes("\0")) {
1157
+ const requestPath = resolve(join(rootPath, relative));
1158
+ const normalizedRoot = resolve(rootPath);
1159
+ if (!requestPath.startsWith(normalizedRoot + sep) && requestPath !== normalizedRoot) {
1303
1160
  return ctx.json({ error: "Forbidden" }, 403);
1304
1161
  }
1305
1162
  if (config.hooks?.onRequest) {
@@ -1427,107 +1284,6 @@ function serveStatic(config, prefix) {
1427
1284
  serveStaticMiddleware.pluginName = "ServeStatic";
1428
1285
  return serveStaticMiddleware;
1429
1286
  }
1430
- class RouterTrie {
1431
- root;
1432
- constructor() {
1433
- this.root = this.createNode();
1434
- }
1435
- createNode() {
1436
- return {
1437
- children: {}
1438
- };
1439
- }
1440
- insert(method, path, handler) {
1441
- let node = this.root;
1442
- const segments = this.splitPath(path);
1443
- for (let i = 0; i < segments.length; i++) {
1444
- const segment = segments[i];
1445
- if (segment === "**") {
1446
- if (!node.recursiveChild) {
1447
- node.recursiveChild = this.createNode();
1448
- }
1449
- node = node.recursiveChild;
1450
- } else if (segment === "*") {
1451
- if (!node.wildcardChild) {
1452
- node.wildcardChild = this.createNode();
1453
- }
1454
- node = node.wildcardChild;
1455
- } else if (segment.startsWith(":")) {
1456
- const paramName = segment.slice(1);
1457
- if (!node.paramChild) {
1458
- node.paramChild = this.createNode();
1459
- node.paramChild.paramName = paramName;
1460
- }
1461
- node = node.paramChild;
1462
- node.paramName = paramName;
1463
- } else {
1464
- if (!node.children[segment]) {
1465
- node.children[segment] = this.createNode();
1466
- }
1467
- node = node.children[segment];
1468
- }
1469
- }
1470
- if (!node.handlers) {
1471
- node.handlers = {};
1472
- }
1473
- node.handlers[method] = handler;
1474
- }
1475
- search(method, path) {
1476
- const segments = this.splitPath(path);
1477
- const params = {};
1478
- const match = this.findNode(this.root, segments, 0, params);
1479
- if (match && match.handlers) {
1480
- const handler = match.handlers[method] || match.handlers["ALL"];
1481
- if (handler) {
1482
- return { handler, params };
1483
- }
1484
- if (method === "HEAD" && match.handlers["GET"]) {
1485
- return { handler: match.handlers["GET"], params };
1486
- }
1487
- }
1488
- return null;
1489
- }
1490
- findNode(node, segments, index, params) {
1491
- if (index === segments.length) {
1492
- if (node.handlers) return node;
1493
- if (node.recursiveChild && node.recursiveChild.handlers) {
1494
- return node.recursiveChild;
1495
- }
1496
- return null;
1497
- }
1498
- const segment = segments[index];
1499
- const child = node.children[segment];
1500
- if (child) {
1501
- const result = this.findNode(child, segments, index + 1, params);
1502
- if (result) return result;
1503
- }
1504
- if (node.paramChild) {
1505
- params[node.paramChild.paramName] = segment;
1506
- const result = this.findNode(node.paramChild, segments, index + 1, params);
1507
- if (result) return result;
1508
- delete params[node.paramChild.paramName];
1509
- }
1510
- if (node.wildcardChild) {
1511
- const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1512
- if (result) return result;
1513
- }
1514
- if (node.recursiveChild) {
1515
- const remaining = segments.length - index;
1516
- for (let k = 0; k <= remaining; k++) {
1517
- const result = this.findNode(node.recursiveChild, segments, index + k, params);
1518
- if (result) return result;
1519
- }
1520
- }
1521
- return null;
1522
- }
1523
- splitPath(path) {
1524
- if (path === "/" || path === "") return [];
1525
- const s = path.startsWith("/") ? path.slice(1) : path;
1526
- if (s === "") return [];
1527
- return s.split("/");
1528
- }
1529
- }
1530
- const asyncContext = new AsyncLocalStorage();
1531
1287
  let db;
1532
1288
  let dbPromise = null;
1533
1289
  let RecordId;
@@ -1591,29 +1347,64 @@ const datastore = {
1591
1347
  process.on("exit", async () => {
1592
1348
  if (db) await db.close();
1593
1349
  });
1594
- const tracer = trace.getTracer("shokupan.middleware");
1595
- function traceHandler(fn, name) {
1596
- return async function(...args) {
1597
- return tracer.startActiveSpan(`route handler - ${name}`, {
1598
- kind: SpanKind.INTERNAL,
1599
- attributes: {
1600
- "http.route": name,
1601
- "component": "shokupan.route"
1602
- }
1603
- }, async (span) => {
1604
- try {
1605
- const result = await fn.apply(this, args);
1606
- return result;
1607
- } catch (err) {
1608
- span.recordException(err);
1609
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
1610
- throw err;
1611
- } finally {
1612
- span.end();
1613
- }
1350
+ class Container {
1351
+ static services = /* @__PURE__ */ new Map();
1352
+ static register(target, instance) {
1353
+ this.services.set(target, instance);
1354
+ }
1355
+ static get(target) {
1356
+ return this.services.get(target);
1357
+ }
1358
+ static has(target) {
1359
+ return this.services.has(target);
1360
+ }
1361
+ static resolve(target) {
1362
+ if (this.services.has(target)) {
1363
+ return this.services.get(target);
1364
+ }
1365
+ const instance = new target();
1366
+ this.services.set(target, instance);
1367
+ return instance;
1368
+ }
1369
+ }
1370
+ function Injectable() {
1371
+ return (target) => {
1372
+ };
1373
+ }
1374
+ function Inject(token) {
1375
+ return (target, key) => {
1376
+ Object.defineProperty(target, key, {
1377
+ get: () => Container.resolve(token),
1378
+ enumerable: true,
1379
+ configurable: true
1614
1380
  });
1615
1381
  };
1616
1382
  }
1383
+ class ShokupanRequestBase {
1384
+ method;
1385
+ url;
1386
+ headers;
1387
+ body;
1388
+ async json() {
1389
+ return JSON.parse(this.body);
1390
+ }
1391
+ async text() {
1392
+ return this.body;
1393
+ }
1394
+ async formData() {
1395
+ if (this.body instanceof FormData) {
1396
+ return this.body;
1397
+ }
1398
+ return new Response(this.body, { headers: this.headers }).formData();
1399
+ }
1400
+ constructor(props) {
1401
+ Object.assign(this, props);
1402
+ if (!(this.headers instanceof Headers)) {
1403
+ this.headers = new Headers(this.headers);
1404
+ }
1405
+ }
1406
+ }
1407
+ const ShokupanRequest = ShokupanRequestBase;
1617
1408
  function getCallerInfo(skipFrames = 1) {
1618
1409
  let file = "unknown";
1619
1410
  let line = 0;
@@ -1639,12 +1430,120 @@ function getCallerInfo(skipFrames = 1) {
1639
1430
  }
1640
1431
  }
1641
1432
  }
1642
- } catch (e) {
1433
+ } catch (e) {
1434
+ }
1435
+ return { file, line };
1436
+ }
1437
+ class RouterTrie {
1438
+ root;
1439
+ constructor() {
1440
+ this.root = this.createNode();
1441
+ }
1442
+ createNode() {
1443
+ return {
1444
+ children: {}
1445
+ };
1446
+ }
1447
+ insert(method, path, handler) {
1448
+ let node = this.root;
1449
+ const segments = this.splitPath(path);
1450
+ for (let i = 0; i < segments.length; i++) {
1451
+ const segment = segments[i];
1452
+ if (segment === "**") {
1453
+ if (!node.recursiveChild) {
1454
+ node.recursiveChild = this.createNode();
1455
+ }
1456
+ node = node.recursiveChild;
1457
+ } else if (segment === "*") {
1458
+ if (!node.wildcardChild) {
1459
+ node.wildcardChild = this.createNode();
1460
+ }
1461
+ node = node.wildcardChild;
1462
+ } else if (segment.startsWith(":")) {
1463
+ const paramName = segment.slice(1);
1464
+ if (!node.paramChild) {
1465
+ node.paramChild = this.createNode();
1466
+ node.paramChild.paramName = paramName;
1467
+ }
1468
+ node = node.paramChild;
1469
+ node.paramName = paramName;
1470
+ } else {
1471
+ if (!node.children[segment]) {
1472
+ node.children[segment] = this.createNode();
1473
+ }
1474
+ node = node.children[segment];
1475
+ }
1476
+ }
1477
+ if (!node.handlers) {
1478
+ node.handlers = {};
1479
+ }
1480
+ node.handlers[method] = handler;
1481
+ }
1482
+ search(method, path) {
1483
+ const segments = this.splitPath(path);
1484
+ const params = {};
1485
+ const match = this.findNode(this.root, segments, 0, params);
1486
+ if (match && match.handlers) {
1487
+ const handler = match.handlers[method] || match.handlers["ALL"];
1488
+ if (handler) {
1489
+ return { handler, params };
1490
+ }
1491
+ if (method === "HEAD" && match.handlers["GET"]) {
1492
+ return { handler: match.handlers["GET"], params };
1493
+ }
1494
+ }
1495
+ return null;
1496
+ }
1497
+ findNode(node, segments, index, params) {
1498
+ if (index === segments.length) {
1499
+ if (node.handlers) return node;
1500
+ if (node.recursiveChild && node.recursiveChild.handlers) {
1501
+ return node.recursiveChild;
1502
+ }
1503
+ return null;
1504
+ }
1505
+ const segment = segments[index];
1506
+ const child = node.children[segment];
1507
+ if (child) {
1508
+ const result = this.findNode(child, segments, index + 1, params);
1509
+ if (result) return result;
1510
+ }
1511
+ if (node.paramChild) {
1512
+ params[node.paramChild.paramName] = segment;
1513
+ const result = this.findNode(node.paramChild, segments, index + 1, params);
1514
+ if (result) return result;
1515
+ delete params[node.paramChild.paramName];
1516
+ }
1517
+ if (node.wildcardChild) {
1518
+ const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1519
+ if (result) return result;
1520
+ }
1521
+ if (node.recursiveChild) {
1522
+ const remaining = segments.length - index;
1523
+ for (let k = 0; k <= remaining; k++) {
1524
+ const result = this.findNode(node.recursiveChild, segments, index + k, params);
1525
+ if (result) return result;
1526
+ }
1527
+ }
1528
+ return null;
1529
+ }
1530
+ splitPath(path) {
1531
+ if (path === "/" || path === "") return [];
1532
+ const s = path.startsWith("/") ? path.slice(1) : path;
1533
+ if (s === "") return [];
1534
+ return s.split("/");
1643
1535
  }
1644
- return { file, line };
1645
1536
  }
1646
- const RouterRegistry = /* @__PURE__ */ new Map();
1647
- const ShokupanApplicationTree = {};
1537
+ const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1538
+ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1539
+ RouteParamType2["BODY"] = "BODY";
1540
+ RouteParamType2["PARAM"] = "PARAM";
1541
+ RouteParamType2["QUERY"] = "QUERY";
1542
+ RouteParamType2["HEADER"] = "HEADER";
1543
+ RouteParamType2["REQUEST"] = "REQUEST";
1544
+ RouteParamType2["CONTEXT"] = "CONTEXT";
1545
+ return RouteParamType2;
1546
+ })(RouteParamType || {});
1648
1547
  class ShokupanRouter {
1649
1548
  constructor(config) {
1650
1549
  this.config = config;
@@ -1755,216 +1654,9 @@ class ShokupanRouter {
1755
1654
  throw new Error(`[Shokupan] strict controller check failed: ${controller.constructor.name || typeof controller} is not a class constructor.`);
1756
1655
  }
1757
1656
  if (this.isRouterInstance(controller)) {
1758
- if (controller[$isMounted]) {
1759
- throw new Error("Router is already mounted");
1760
- }
1761
- controller[$mountPath] = prefix;
1762
- if (!controller.metadata) {
1763
- const info = getCallerInfo();
1764
- controller.metadata = {
1765
- file: info.file,
1766
- line: info.line,
1767
- name: "MountedRouter"
1768
- };
1769
- }
1770
- this[$childRouters].push(controller);
1771
- controller[$parent] = this;
1772
- const setRouterContext = (router) => {
1773
- router[$appRoot] = this.root;
1774
- router[$childRouters].forEach((child) => setRouterContext(child));
1775
- };
1776
- setRouterContext(controller);
1777
- if (this[$appRoot]) ;
1778
- controller[$appRoot] = this.root;
1779
- controller[$isMounted] = true;
1657
+ this.mountRouter(prefix, controller);
1780
1658
  } else {
1781
- let instance = controller;
1782
- if (typeof controller === "function") {
1783
- instance = Container.resolve(controller);
1784
- const controllerPath = controller[$controllerPath];
1785
- if (controllerPath) {
1786
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1787
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1788
- prefix = p1 + p2;
1789
- if (!prefix) prefix = "/";
1790
- }
1791
- } else {
1792
- const ctor = instance.constructor;
1793
- const controllerPath = ctor[$controllerPath];
1794
- if (controllerPath) {
1795
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1796
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1797
- prefix = p1 + p2;
1798
- if (!prefix) prefix = "/";
1799
- }
1800
- }
1801
- instance[$mountPath] = prefix;
1802
- const info = getCallerInfo();
1803
- instance.metadata = {
1804
- file: info.file,
1805
- line: info.line,
1806
- name: instance.constructor.name
1807
- };
1808
- this[$childControllers].push(instance);
1809
- const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1810
- const proto = Object.getPrototypeOf(instance);
1811
- const methods = /* @__PURE__ */ new Set();
1812
- let current = proto;
1813
- while (current && current !== Object.prototype) {
1814
- Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1815
- current = Object.getPrototypeOf(current);
1816
- }
1817
- Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1818
- const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1819
- const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1820
- const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1821
- let routesAttached = 0;
1822
- for (let i = 0; i < Array.from(methods).length; i++) {
1823
- const name = Array.from(methods)[i];
1824
- if (name === "constructor") continue;
1825
- if (["arguments", "caller", "callee"].includes(name)) continue;
1826
- const originalHandler = instance[name];
1827
- if (typeof originalHandler !== "function") continue;
1828
- let method;
1829
- let subPath = "";
1830
- if (decoratedRoutes && decoratedRoutes.has(name)) {
1831
- const config = decoratedRoutes.get(name);
1832
- method = config.method;
1833
- subPath = config.path;
1834
- } else {
1835
- for (let j = 0; j < HTTPMethods.length; j++) {
1836
- const m = HTTPMethods[j];
1837
- if (name.toUpperCase().startsWith(m)) {
1838
- method = m;
1839
- const rest = name.slice(m.length);
1840
- if (rest.length === 0) {
1841
- subPath = "/";
1842
- } else {
1843
- subPath = "";
1844
- let buffer = "";
1845
- const flush = () => {
1846
- if (buffer.length > 0) {
1847
- subPath += "/" + buffer.toLowerCase();
1848
- buffer = "";
1849
- }
1850
- };
1851
- for (let i2 = 0; i2 < rest.length; i2++) {
1852
- const char = rest[i2];
1853
- if (char === "$") {
1854
- flush();
1855
- subPath += "/:";
1856
- continue;
1857
- }
1858
- }
1859
- subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1860
- if (!subPath.startsWith("/")) {
1861
- subPath = "/" + subPath;
1862
- }
1863
- }
1864
- break;
1865
- }
1866
- }
1867
- }
1868
- if (method) {
1869
- routesAttached++;
1870
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1871
- const cleanSubPath = subPath === "/" ? "" : subPath;
1872
- let joined;
1873
- if (cleanSubPath.length === 0) {
1874
- joined = cleanPrefix;
1875
- } else if (cleanSubPath.startsWith("/")) {
1876
- joined = cleanPrefix + cleanSubPath;
1877
- } else {
1878
- joined = cleanPrefix + "/" + cleanSubPath;
1879
- }
1880
- const fullPath = joined || "/";
1881
- const normalizedPath = fullPath.replace(/\/+/g, "/");
1882
- const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1883
- const allMiddleware = [...controllerMiddleware, ...methodMw];
1884
- const routeArgs = decoratedArgs && decoratedArgs.get(name);
1885
- const wrappedHandler = async (ctx) => {
1886
- let args = [ctx];
1887
- if (routeArgs?.length > 0) {
1888
- args = [];
1889
- const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1890
- for (let k = 0; k < sortedArgs.length; k++) {
1891
- const arg = sortedArgs[k];
1892
- switch (arg.type) {
1893
- case RouteParamType.BODY:
1894
- try {
1895
- if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1896
- args[arg.index] = await ctx.req.json();
1897
- } else {
1898
- const text = await ctx.req.text();
1899
- if (!text) {
1900
- args[arg.index] = {};
1901
- } else {
1902
- args[arg.index] = JSON.parse(text);
1903
- }
1904
- }
1905
- } catch (e) {
1906
- const err = new Error("Invalid JSON body");
1907
- err.status = 400;
1908
- throw err;
1909
- }
1910
- break;
1911
- case RouteParamType.PARAM:
1912
- args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1913
- break;
1914
- case RouteParamType.QUERY: {
1915
- const url = new URL(ctx.req.url);
1916
- if (arg.name) {
1917
- const vals = url.searchParams.getAll(arg.name);
1918
- args[arg.index] = vals.length > 1 ? vals : vals[0];
1919
- } else {
1920
- const query = {};
1921
- const keys = Object.keys(url.searchParams);
1922
- for (let k2 = 0; k2 < keys.length; k2++) {
1923
- const key = keys[k2];
1924
- const vals = url.searchParams.getAll(key);
1925
- query[key] = vals.length > 1 ? vals : vals[0];
1926
- }
1927
- args[arg.index] = query;
1928
- }
1929
- break;
1930
- }
1931
- case RouteParamType.HEADER:
1932
- args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
1933
- break;
1934
- case RouteParamType.REQUEST:
1935
- args[arg.index] = ctx.req;
1936
- break;
1937
- case RouteParamType.CONTEXT:
1938
- args[arg.index] = ctx;
1939
- break;
1940
- }
1941
- }
1942
- }
1943
- const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1944
- return tracedOriginalHandler.apply(instance, args);
1945
- };
1946
- let finalHandler = wrappedHandler;
1947
- if (allMiddleware.length > 0) {
1948
- const composed = compose(allMiddleware);
1949
- finalHandler = async (ctx) => {
1950
- return composed(ctx, () => wrappedHandler(ctx));
1951
- };
1952
- }
1953
- finalHandler.originalHandler = originalHandler;
1954
- if (finalHandler !== wrappedHandler) {
1955
- wrappedHandler.originalHandler = originalHandler;
1956
- }
1957
- const tagName = instance.constructor.name;
1958
- const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
1959
- const userSpec = decoratedSpecs && decoratedSpecs.get(name);
1960
- const spec = { tags: [tagName], ...userSpec };
1961
- this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
1962
- }
1963
- }
1964
- if (routesAttached === 0) {
1965
- console.warn(`No routes attached to controller ${instance.constructor.name}`);
1966
- }
1967
- instance[$isMounted] = true;
1659
+ this.scanControllerRoutes(prefix, controller);
1968
1660
  }
1969
1661
  return this;
1970
1662
  }
@@ -2002,8 +1694,6 @@ class ShokupanRouter {
2002
1694
  */
2003
1695
  async internalRequest(arg) {
2004
1696
  const options = typeof arg === "string" ? { path: arg } : arg;
2005
- const store = asyncContext.getStore();
2006
- store?.get("req");
2007
1697
  let url = options.path;
2008
1698
  if (!url.startsWith("http")) {
2009
1699
  const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig.port || 3e3}`;
@@ -2115,6 +1805,220 @@ class ShokupanRouter {
2115
1805
  wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
2116
1806
  return wrapped;
2117
1807
  }
1808
+ mountRouter(prefix, router) {
1809
+ if (router[$isMounted]) {
1810
+ throw new Error("Router is already mounted");
1811
+ }
1812
+ router[$mountPath] = prefix;
1813
+ if (!router.metadata) {
1814
+ const info = getCallerInfo();
1815
+ router.metadata = {
1816
+ file: info.file,
1817
+ line: info.line,
1818
+ name: "MountedRouter"
1819
+ };
1820
+ }
1821
+ this[$childRouters].push(router);
1822
+ router[$parent] = this;
1823
+ const setRouterContext = (router2) => {
1824
+ router2[$appRoot] = this.root;
1825
+ router2[$childRouters].forEach((child) => setRouterContext(child));
1826
+ };
1827
+ setRouterContext(router);
1828
+ router[$appRoot] = this.root;
1829
+ router[$isMounted] = true;
1830
+ }
1831
+ scanControllerRoutes(prefix, controller) {
1832
+ let instance = controller;
1833
+ if (typeof controller === "function") {
1834
+ instance = Container.resolve(controller);
1835
+ const controllerPath = controller[$controllerPath];
1836
+ if (controllerPath) {
1837
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1838
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1839
+ prefix = p1 + p2;
1840
+ if (!prefix) prefix = "/";
1841
+ }
1842
+ } else {
1843
+ const ctor = instance.constructor;
1844
+ const controllerPath = ctor[$controllerPath];
1845
+ if (controllerPath) {
1846
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1847
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1848
+ prefix = p1 + p2;
1849
+ if (!prefix) prefix = "/";
1850
+ }
1851
+ }
1852
+ instance[$mountPath] = prefix;
1853
+ const info = getCallerInfo();
1854
+ instance.metadata = {
1855
+ file: info.file,
1856
+ line: info.line,
1857
+ name: instance.constructor.name
1858
+ };
1859
+ this[$childControllers].push(instance);
1860
+ const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1861
+ const proto = Object.getPrototypeOf(instance);
1862
+ const methods = /* @__PURE__ */ new Set();
1863
+ let current = proto;
1864
+ while (current && current !== Object.prototype) {
1865
+ Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1866
+ current = Object.getPrototypeOf(current);
1867
+ }
1868
+ Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1869
+ const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1870
+ const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1871
+ const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1872
+ let routesAttached = 0;
1873
+ for (let i = 0; i < Array.from(methods).length; i++) {
1874
+ const name = Array.from(methods)[i];
1875
+ if (name === "constructor") continue;
1876
+ if (["arguments", "caller", "callee"].includes(name)) continue;
1877
+ const originalHandler = instance[name];
1878
+ if (typeof originalHandler !== "function") continue;
1879
+ let method;
1880
+ let subPath = "";
1881
+ if (decoratedRoutes && decoratedRoutes.has(name)) {
1882
+ const config = decoratedRoutes.get(name);
1883
+ method = config.method;
1884
+ subPath = config.path;
1885
+ } else {
1886
+ for (let j = 0; j < HTTPMethods.length; j++) {
1887
+ const m = HTTPMethods[j];
1888
+ if (name.toUpperCase().startsWith(m)) {
1889
+ method = m;
1890
+ const rest = name.slice(m.length);
1891
+ if (rest.length === 0) {
1892
+ subPath = "/";
1893
+ } else {
1894
+ subPath = "";
1895
+ let buffer = "";
1896
+ const flush = () => {
1897
+ if (buffer.length > 0) {
1898
+ subPath += "/" + buffer.toLowerCase();
1899
+ buffer = "";
1900
+ }
1901
+ };
1902
+ for (let i2 = 0; i2 < rest.length; i2++) {
1903
+ const char = rest[i2];
1904
+ if (char === "$") {
1905
+ flush();
1906
+ subPath += "/:";
1907
+ continue;
1908
+ }
1909
+ buffer += char;
1910
+ }
1911
+ if (buffer.length > 0) flush();
1912
+ subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1913
+ if (!subPath.startsWith("/")) {
1914
+ subPath = "/" + subPath;
1915
+ }
1916
+ }
1917
+ break;
1918
+ }
1919
+ }
1920
+ }
1921
+ if (method) {
1922
+ routesAttached++;
1923
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1924
+ const cleanSubPath = subPath === "/" ? "" : subPath;
1925
+ let joined;
1926
+ if (cleanSubPath.length === 0) {
1927
+ joined = cleanPrefix;
1928
+ } else if (cleanSubPath.startsWith("/")) {
1929
+ joined = cleanPrefix + cleanSubPath;
1930
+ } else {
1931
+ joined = cleanPrefix + "/" + cleanSubPath;
1932
+ }
1933
+ const fullPath = joined || "/";
1934
+ const normalizedPath = fullPath.replace(/\/+/g, "/");
1935
+ const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1936
+ const allMiddleware = [...controllerMiddleware, ...methodMw];
1937
+ const routeArgs = decoratedArgs && decoratedArgs.get(name);
1938
+ const wrappedHandler = async (ctx) => {
1939
+ let args = [ctx];
1940
+ if (routeArgs?.length > 0) {
1941
+ args = [];
1942
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1943
+ for (let k = 0; k < sortedArgs.length; k++) {
1944
+ const arg = sortedArgs[k];
1945
+ switch (arg.type) {
1946
+ case RouteParamType.BODY:
1947
+ try {
1948
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1949
+ args[arg.index] = await ctx.req.json();
1950
+ } else {
1951
+ const text = await ctx.req.text();
1952
+ if (!text) {
1953
+ args[arg.index] = {};
1954
+ } else {
1955
+ args[arg.index] = JSON.parse(text);
1956
+ }
1957
+ }
1958
+ } catch (e) {
1959
+ const err = new Error("Invalid JSON body");
1960
+ err.status = 400;
1961
+ throw err;
1962
+ }
1963
+ break;
1964
+ case RouteParamType.PARAM:
1965
+ args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1966
+ break;
1967
+ case RouteParamType.QUERY: {
1968
+ const url = new URL(ctx.req.url);
1969
+ if (arg.name) {
1970
+ const vals = url.searchParams.getAll(arg.name);
1971
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
1972
+ } else {
1973
+ const query = {};
1974
+ const keys = Object.keys(url.searchParams);
1975
+ for (let k2 = 0; k2 < keys.length; k2++) {
1976
+ const key = keys[k2];
1977
+ const vals = url.searchParams.getAll(key);
1978
+ query[key] = vals.length > 1 ? vals : vals[0];
1979
+ }
1980
+ args[arg.index] = query;
1981
+ }
1982
+ break;
1983
+ }
1984
+ case RouteParamType.HEADER:
1985
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
1986
+ break;
1987
+ case RouteParamType.REQUEST:
1988
+ args[arg.index] = ctx.req;
1989
+ break;
1990
+ case RouteParamType.CONTEXT:
1991
+ args[arg.index] = ctx;
1992
+ break;
1993
+ }
1994
+ }
1995
+ }
1996
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1997
+ return tracedOriginalHandler.apply(instance, args);
1998
+ };
1999
+ let finalHandler = wrappedHandler;
2000
+ if (allMiddleware.length > 0) {
2001
+ const composed = compose(allMiddleware);
2002
+ finalHandler = async (ctx) => {
2003
+ return composed(ctx, () => wrappedHandler(ctx));
2004
+ };
2005
+ }
2006
+ finalHandler.originalHandler = originalHandler;
2007
+ if (finalHandler !== wrappedHandler) {
2008
+ wrappedHandler.originalHandler = originalHandler;
2009
+ }
2010
+ const tagName = instance.constructor.name;
2011
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2012
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2013
+ const spec = { tags: [tagName], ...userSpec };
2014
+ this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2015
+ }
2016
+ }
2017
+ if (routesAttached === 0) {
2018
+ console.warn(`No routes attached to controller ${instance.constructor.name}`);
2019
+ }
2020
+ instance[$isMounted] = true;
2021
+ }
2118
2022
  /**
2119
2023
  * Find a route matching the given method and path.
2120
2024
  * @param method HTTP method
@@ -2148,10 +2052,13 @@ class ShokupanRouter {
2148
2052
  }
2149
2053
  parsePath(path) {
2150
2054
  const keys = [];
2055
+ if (path.length > 2048) {
2056
+ throw new Error("Path too long");
2057
+ }
2151
2058
  const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
2152
2059
  keys.push(key);
2153
- return "([^/]+)";
2154
- }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
2060
+ return "([^/]{1,255})";
2061
+ }).replace(/\*\*/g, ".{0,1000}").replace(/\*/g, "[^/]{1,255}");
2155
2062
  return {
2156
2063
  regex: new RegExp(`^${pattern}$`),
2157
2064
  keys
@@ -2238,7 +2145,7 @@ class ShokupanRouter {
2238
2145
  if (effectiveRenderer) {
2239
2146
  const innerHandler = wrappedHandler;
2240
2147
  wrappedHandler = async (ctx) => {
2241
- ctx.renderer = effectiveRenderer;
2148
+ ctx.setRenderer(effectiveRenderer);
2242
2149
  return innerHandler(ctx);
2243
2150
  };
2244
2151
  }
@@ -2640,6 +2547,13 @@ class Shokupan extends ShokupanRouter {
2640
2547
  }
2641
2548
  return this;
2642
2549
  }
2550
+ /**
2551
+ * Registers a plugin.
2552
+ */
2553
+ register(plugin, options) {
2554
+ plugin.onInit(this, options);
2555
+ return this;
2556
+ }
2643
2557
  startupHooks = [];
2644
2558
  /**
2645
2559
  * Registers a callback to be executed before the server starts listening.
@@ -2702,7 +2616,7 @@ class Shokupan extends ShokupanRouter {
2702
2616
  };
2703
2617
  let factory = this.applicationConfig.serverFactory;
2704
2618
  if (!factory && typeof Bun === "undefined") {
2705
- const { createHttpServer } = await import("./server-adapter-0xH174zz.js");
2619
+ const { createHttpServer } = await import("./http-server-0xH174zz.js");
2706
2620
  factory = createHttpServer();
2707
2621
  }
2708
2622
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2771,19 +2685,19 @@ class Shokupan extends ShokupanRouter {
2771
2685
  "http.method": req.method
2772
2686
  }
2773
2687
  };
2774
- const parent = store?.get("span");
2688
+ const parent = store?.span;
2775
2689
  const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
2776
2690
  return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2777
- const ctxMap = /* @__PURE__ */ new Map();
2778
- ctxMap.set("span", span);
2779
- ctxMap.set("request", req);
2780
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
2691
+ const ctxStore = new RequestContextStore();
2692
+ ctxStore.span = span;
2693
+ ctxStore.request = req;
2694
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server).finally(() => span.end()));
2781
2695
  });
2782
2696
  }
2783
2697
  if (this.applicationConfig.enableAsyncLocalStorage) {
2784
- const ctxMap = /* @__PURE__ */ new Map();
2785
- ctxMap.set("request", req);
2786
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
2698
+ const ctxStore = new RequestContextStore();
2699
+ ctxStore.request = req;
2700
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server));
2787
2701
  }
2788
2702
  return this.handleRequest(req, server);
2789
2703
  }
@@ -2805,6 +2719,7 @@ class Shokupan extends ShokupanRouter {
2805
2719
  const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2806
2720
  const match = this.find(req.method, ctx.path);
2807
2721
  if (match) {
2722
+ ctx._routeMatched = true;
2808
2723
  ctx.params = match.params;
2809
2724
  await bodyParsing;
2810
2725
  return match.handler(ctx);
@@ -2819,10 +2734,14 @@ class Shokupan extends ShokupanRouter {
2819
2734
  } else if (result === null || result === void 0) {
2820
2735
  if (ctx._finalResponse instanceof Response) {
2821
2736
  response = ctx._finalResponse;
2822
- } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
2737
+ } else if (ctx._routeMatched) {
2823
2738
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2824
2739
  } else {
2825
- response = ctx.text("Not Found", 404);
2740
+ if (ctx.response.status !== 200) {
2741
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2742
+ } else {
2743
+ response = ctx.text("Not Found", 404);
2744
+ }
2826
2745
  }
2827
2746
  } else if (typeof result === "object") {
2828
2747
  response = ctx.json(result);
@@ -2834,7 +2753,7 @@ class Shokupan extends ShokupanRouter {
2834
2753
  return response;
2835
2754
  } catch (err) {
2836
2755
  console.error(err);
2837
- const span = asyncContext.getStore()?.get("span");
2756
+ const span = asyncContext.getStore()?.span;
2838
2757
  if (span) span.setStatus({ code: 2 });
2839
2758
  const status = err.status || err.statusCode || 500;
2840
2759
  const body = { error: err.message || "Internal Server Error" };
@@ -2842,31 +2761,195 @@ class Shokupan extends ShokupanRouter {
2842
2761
  await this.runHooks("onError", ctx, err);
2843
2762
  return ctx.json(body, status);
2844
2763
  }
2845
- };
2846
- let executionPromise = handle();
2847
- const timeoutMs = this.applicationConfig.requestTimeout;
2848
- if (timeoutMs && timeoutMs > 0) {
2849
- let timeoutId;
2850
- const timeoutPromise = new Promise((_, reject) => {
2851
- timeoutId = setTimeout(async () => {
2852
- controller.abort();
2853
- await this.runHooks("onRequestTimeout", ctx);
2854
- reject(new Error("Request Timeout"));
2855
- }, timeoutMs);
2764
+ };
2765
+ let executionPromise = handle();
2766
+ const timeoutMs = this.applicationConfig.requestTimeout;
2767
+ if (timeoutMs && timeoutMs > 0) {
2768
+ let timeoutId;
2769
+ const timeoutPromise = new Promise((_, reject) => {
2770
+ timeoutId = setTimeout(async () => {
2771
+ controller.abort();
2772
+ await this.runHooks("onRequestTimeout", ctx);
2773
+ reject(new Error("Request Timeout"));
2774
+ }, timeoutMs);
2775
+ });
2776
+ executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2777
+ }
2778
+ return executionPromise.catch((err) => {
2779
+ if (err.message === "Request Timeout") {
2780
+ return ctx.text("Request Timeout", 408);
2781
+ }
2782
+ console.error("Unexpected error in request execution:", err);
2783
+ return ctx.text("Internal Server Error", 500);
2784
+ }).then(async (res) => {
2785
+ await this.runHooks("onResponseEnd", ctx, res);
2786
+ return res;
2787
+ });
2788
+ }
2789
+ }
2790
+ function RateLimitMiddleware(options = {}) {
2791
+ const windowMs = options.windowMs || 60 * 1e3;
2792
+ const max = options.limit || options.max || 5;
2793
+ const message = options.message || "Too many requests, please try again later.";
2794
+ const statusCode = options.statusCode || 429;
2795
+ const headers = options.headers !== false;
2796
+ const mode = options.mode || "user";
2797
+ const trustedProxies = options.trustedProxies || [];
2798
+ const keyGenerator = options.keyGenerator || ((ctx) => {
2799
+ if (mode === "absolute") {
2800
+ return "global";
2801
+ }
2802
+ const xForwardedFor = ctx.headers.get("x-forwarded-for");
2803
+ if (xForwardedFor && trustedProxies.length > 0) {
2804
+ const ips = xForwardedFor.split(",").map((ip) => ip.trim());
2805
+ for (let i = ips.length - 1; i >= 0; i--) {
2806
+ const ip = ips[i];
2807
+ if (!trustedProxies.includes(ip)) {
2808
+ if (/^[\d.:a-fA-F]+$/.test(ip)) {
2809
+ return ip;
2810
+ }
2811
+ }
2812
+ }
2813
+ }
2814
+ return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
2815
+ });
2816
+ const skip = options.skip || (() => false);
2817
+ const hits = /* @__PURE__ */ new Map();
2818
+ const interval = setInterval(() => {
2819
+ const now = Date.now();
2820
+ const entries = Array.from(hits.entries());
2821
+ for (let i = 0; i < entries.length; i++) {
2822
+ const [key, record] = entries[i];
2823
+ if (record.resetTime <= now) {
2824
+ hits.delete(key);
2825
+ }
2826
+ }
2827
+ }, windowMs);
2828
+ if (interval.unref) interval.unref();
2829
+ const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
2830
+ if (skip(ctx)) return next();
2831
+ const key = keyGenerator(ctx);
2832
+ const now = Date.now();
2833
+ let record = hits.get(key);
2834
+ if (!record || record.resetTime <= now) {
2835
+ record = {
2836
+ hits: 0,
2837
+ resetTime: now + windowMs
2838
+ };
2839
+ hits.set(key, record);
2840
+ }
2841
+ record.hits++;
2842
+ const remaining = Math.max(0, max - record.hits);
2843
+ const resetTime = Math.ceil(record.resetTime / 1e3);
2844
+ const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
2845
+ const setHeaders = (res) => {
2846
+ if (!headers || !res || !res.headers) return;
2847
+ try {
2848
+ res.headers.set("X-RateLimit-Limit", String(max));
2849
+ res.headers.set("X-RateLimit-Remaining", String(remaining));
2850
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
2851
+ } catch (e) {
2852
+ }
2853
+ };
2854
+ if (record.hits > max) {
2855
+ if (options.onRateLimited) {
2856
+ const result = await options.onRateLimited(ctx, key);
2857
+ if (result instanceof Response) {
2858
+ return result;
2859
+ }
2860
+ }
2861
+ const msg = typeof message === "function" ? message(ctx, key) : message;
2862
+ typeof msg === "object" ? JSON.stringify(msg) : String(msg);
2863
+ const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
2864
+ if (headers) {
2865
+ setHeaders(res);
2866
+ res.headers.set("Retry-After", String(retryAfter));
2867
+ }
2868
+ return res;
2869
+ }
2870
+ const response = await next();
2871
+ if (response instanceof Response && headers) {
2872
+ setHeaders(response);
2873
+ }
2874
+ return response;
2875
+ };
2876
+ rateLimitMiddleware.isBuiltin = true;
2877
+ rateLimitMiddleware.pluginName = "RateLimit";
2878
+ return rateLimitMiddleware;
2879
+ }
2880
+ function Controller(path = "/") {
2881
+ return (target) => {
2882
+ target[$controllerPath] = path;
2883
+ };
2884
+ }
2885
+ function Use(...middleware) {
2886
+ return (target, propertyKey, descriptor) => {
2887
+ if (!propertyKey) {
2888
+ const existing = target[$middleware] || [];
2889
+ target[$middleware] = [...existing, ...middleware];
2890
+ } else {
2891
+ if (!target[$middleware]) {
2892
+ target[$middleware] = /* @__PURE__ */ new Map();
2893
+ }
2894
+ const existing = target[$middleware].get(propertyKey) || [];
2895
+ target[$middleware].set(propertyKey, [...existing, ...middleware]);
2896
+ }
2897
+ };
2898
+ }
2899
+ function createParamDecorator(type) {
2900
+ return (name) => {
2901
+ return (target, propertyKey, parameterIndex) => {
2902
+ if (!target[$routeArgs]) {
2903
+ target[$routeArgs] = /* @__PURE__ */ new Map();
2904
+ }
2905
+ if (!target[$routeArgs].has(propertyKey)) {
2906
+ target[$routeArgs].set(propertyKey, []);
2907
+ }
2908
+ target[$routeArgs].get(propertyKey).push({
2909
+ index: parameterIndex,
2910
+ type,
2911
+ name
2856
2912
  });
2857
- executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2913
+ };
2914
+ };
2915
+ }
2916
+ const Body = createParamDecorator(RouteParamType.BODY);
2917
+ const Param = createParamDecorator(RouteParamType.PARAM);
2918
+ const Query = createParamDecorator(RouteParamType.QUERY);
2919
+ const Headers$1 = createParamDecorator(RouteParamType.HEADER);
2920
+ const Req = createParamDecorator(RouteParamType.REQUEST);
2921
+ const Ctx = createParamDecorator(RouteParamType.CONTEXT);
2922
+ function Spec(spec) {
2923
+ return (target, propertyKey, descriptor) => {
2924
+ if (!target[$routeSpec]) {
2925
+ target[$routeSpec] = /* @__PURE__ */ new Map();
2858
2926
  }
2859
- return executionPromise.catch((err) => {
2860
- if (err.message === "Request Timeout") {
2861
- return ctx.text("Request Timeout", 408);
2927
+ target[$routeSpec].set(propertyKey, spec);
2928
+ };
2929
+ }
2930
+ function createMethodDecorator(method) {
2931
+ return (path = "/") => {
2932
+ return (target, propertyKey, descriptor) => {
2933
+ if (!target[$routeMethods]) {
2934
+ target[$routeMethods] = /* @__PURE__ */ new Map();
2862
2935
  }
2863
- console.error("Unexpected error in request execution:", err);
2864
- return ctx.text("Internal Server Error", 500);
2865
- }).then(async (res) => {
2866
- await this.runHooks("onResponseEnd", ctx, res);
2867
- return res;
2868
- });
2869
- }
2936
+ target[$routeMethods].set(propertyKey, {
2937
+ method,
2938
+ path
2939
+ });
2940
+ };
2941
+ };
2942
+ }
2943
+ const Get = createMethodDecorator("GET");
2944
+ const Post = createMethodDecorator("POST");
2945
+ const Put = createMethodDecorator("PUT");
2946
+ const Delete = createMethodDecorator("DELETE");
2947
+ const Patch = createMethodDecorator("PATCH");
2948
+ const Options = createMethodDecorator("OPTIONS");
2949
+ const Head = createMethodDecorator("HEAD");
2950
+ const All = createMethodDecorator("ALL");
2951
+ function RateLimit(options) {
2952
+ return Use(RateLimitMiddleware(options));
2870
2953
  }
2871
2954
  class AuthPlugin extends ShokupanRouter {
2872
2955
  constructor(authConfig) {
@@ -2876,6 +2959,13 @@ class AuthPlugin extends ShokupanRouter {
2876
2959
  this.init();
2877
2960
  }
2878
2961
  secret;
2962
+ onInit(app, options) {
2963
+ if (options?.path) {
2964
+ app.mount(options.path, this);
2965
+ } else {
2966
+ app.mount(options.path ?? "/", this);
2967
+ }
2968
+ }
2879
2969
  getProviderInstance(name, p) {
2880
2970
  switch (name) {
2881
2971
  case "github":
@@ -2939,9 +3029,10 @@ class AuthPlugin extends ShokupanRouter {
2939
3029
  } else {
2940
3030
  return ctx.text("Provider config error", 500);
2941
3031
  }
2942
- ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; Max-Age=600`);
3032
+ const isSecure = ctx.secure;
3033
+ ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
2943
3034
  if (codeVerifier) {
2944
- ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; Max-Age=600`);
3035
+ ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
2945
3036
  }
2946
3037
  return ctx.redirect(url.toString());
2947
3038
  });
@@ -2982,7 +3073,7 @@ class AuthPlugin extends ShokupanRouter {
2982
3073
  return ctx.json({ token: jwt, user });
2983
3074
  } catch (e) {
2984
3075
  console.error("Auth Error", e);
2985
- return ctx.text("Authentication failed: " + e.message + "\n" + e.stack, 500);
3076
+ return ctx.text("Authentication failed. Please try again.", 500);
2986
3077
  }
2987
3078
  });
2988
3079
  }
@@ -3092,8 +3183,196 @@ class AuthPlugin extends ShokupanRouter {
3092
3183
  };
3093
3184
  }
3094
3185
  }
3186
+ class ClusterPlugin {
3187
+ constructor(options = {}) {
3188
+ this.options = options;
3189
+ }
3190
+ onInit(app) {
3191
+ const originalListen = app.listen.bind(app);
3192
+ const { workers = "auto", silent = false, sticky = false } = this.options;
3193
+ const isBun = typeof Bun !== "undefined";
3194
+ const numCPUs = os__default.cpus().length;
3195
+ const numWorkers = workers === "auto" || workers === -1 ? numCPUs : workers;
3196
+ if (numWorkers <= 1) {
3197
+ return;
3198
+ }
3199
+ app.listen = async (port) => {
3200
+ const finalPort = port ?? app.applicationConfig.port ?? 3e3;
3201
+ if (isBun) {
3202
+ return this.handleBun(app, finalPort, numWorkers, originalListen);
3203
+ } else {
3204
+ return this.handleNode(app, finalPort, numWorkers, originalListen, silent, sticky);
3205
+ }
3206
+ };
3207
+ }
3208
+ async handleBun(app, port, workers, originalListen) {
3209
+ const workerId = process.env["SHOKUPAN_WORKER_ID"];
3210
+ if (workerId) {
3211
+ app.applicationConfig.reusePort = true;
3212
+ return originalListen(port);
3213
+ }
3214
+ console.log(`[Cluster] Starting ${workers} Bun workers on port ${port}...`);
3215
+ const spawnWorker = (id) => {
3216
+ Bun.spawn([process.argv0, ...process.argv.slice(1)], {
3217
+ env: { ...process.env, SHOKUPAN_WORKER_ID: id },
3218
+ stdio: ["inherit", "inherit", "inherit"],
3219
+ onExit(proc, exitCode, signalCode, error) {
3220
+ console.log(`[Cluster] Worker ${id} died (code: ${exitCode}). Restarting...`);
3221
+ spawnWorker(id);
3222
+ }
3223
+ });
3224
+ };
3225
+ for (let i = 0; i < workers; i++) {
3226
+ spawnWorker(process.pid + "_" + i + 1);
3227
+ }
3228
+ setInterval(() => {
3229
+ }, 1e3 * 60 * 60);
3230
+ return {
3231
+ stop: () => {
3232
+ },
3233
+ port
3234
+ };
3235
+ }
3236
+ async handleNode(app, port, workers, originalListen, silent, sticky) {
3237
+ if (cluster.isPrimary) {
3238
+ console.log(`[Cluster] Master ${process.pid} is running`);
3239
+ const fork = () => cluster.fork(process.env);
3240
+ for (let i = 0; i < workers; i++) {
3241
+ fork();
3242
+ }
3243
+ cluster.on("exit", (worker, code, signal) => {
3244
+ console.log(`[Cluster] Worker ${worker.process.pid} died. Restarting...`);
3245
+ fork();
3246
+ });
3247
+ if (sticky) {
3248
+ const server = net.createServer({ pauseOnConnect: true }, (connection) => {
3249
+ const remote = connection.remoteAddress || "";
3250
+ let hash = 0;
3251
+ for (let i = 0; i < remote.length; i++) {
3252
+ hash = (hash << 5) - hash + remote.charCodeAt(i);
3253
+ hash |= 0;
3254
+ }
3255
+ const index = Math.abs(hash) % workers;
3256
+ const worker = Object.values(cluster.workers)[index];
3257
+ if (worker) {
3258
+ worker.send("sticky-session:connection", connection);
3259
+ } else {
3260
+ connection.end();
3261
+ }
3262
+ });
3263
+ server.listen(port, () => {
3264
+ console.log(`[Cluster] Sticky Load Balancer listening on port ${port}`);
3265
+ });
3266
+ return {
3267
+ close: () => server.close(),
3268
+ port
3269
+ };
3270
+ } else {
3271
+ return {
3272
+ close: () => {
3273
+ },
3274
+ // Master controls
3275
+ port
3276
+ };
3277
+ }
3278
+ } else {
3279
+ if (sticky) {
3280
+ const server = await originalListen(0);
3281
+ process.on("message", (message, handle) => {
3282
+ if (message !== "sticky-session:connection") return;
3283
+ if (!handle) return;
3284
+ server.emit("connection", handle);
3285
+ handle.resume();
3286
+ });
3287
+ return server;
3288
+ } else {
3289
+ return originalListen(port);
3290
+ }
3291
+ }
3292
+ }
3293
+ }
3294
+ const eta = new Eta();
3295
+ class ScalarPlugin extends ShokupanRouter {
3296
+ constructor(pluginOptions = {}) {
3297
+ pluginOptions.config ??= {};
3298
+ super();
3299
+ this.pluginOptions = pluginOptions;
3300
+ this.init();
3301
+ }
3302
+ onInit(app, options) {
3303
+ if (options?.path) {
3304
+ app.mount(options.path, this);
3305
+ } else {
3306
+ app.mount(options.path ?? "/", this);
3307
+ }
3308
+ this.onMount(app);
3309
+ }
3310
+ init() {
3311
+ this.get("/", (ctx) => {
3312
+ let path = ctx.url.toString();
3313
+ if (!path.endsWith("/")) path += "/";
3314
+ return ctx.html(eta.renderString(`<!doctype html>
3315
+ <html>
3316
+ <head>
3317
+ <title>API Reference</title>
3318
+ <meta charset = "utf-8" />
3319
+ <meta name="viewport" content = "width=device-width, initial-scale=1" />
3320
+ </head>
3321
+
3322
+ <body>
3323
+ <div id="app"></div>
3324
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
3325
+ <script>
3326
+ Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3327
+ url: "<%= it.path %>openapi.json",
3328
+ }
3329
+ ])
3330
+ <\/script>
3331
+ </body>
3332
+
3333
+ </html>`, { path, config: this.pluginOptions }));
3334
+ });
3335
+ this.get("/openapi.json", async (ctx) => {
3336
+ let spec;
3337
+ if (this.root.openApiSpec) {
3338
+ try {
3339
+ spec = structuredClone(this.root.openApiSpec);
3340
+ } catch (e) {
3341
+ spec = Object.assign({}, this.root.openApiSpec);
3342
+ }
3343
+ } else {
3344
+ spec = await (this.root || this).generateApiSpec();
3345
+ }
3346
+ if (this.pluginOptions.baseDocument) {
3347
+ deepMerge(spec, this.pluginOptions.baseDocument);
3348
+ }
3349
+ return ctx.json(spec);
3350
+ });
3351
+ }
3352
+ // New lifecycle method to be called by router.mount
3353
+ onMount(parent) {
3354
+ if (parent.onStart) {
3355
+ parent.onStart(async () => {
3356
+ if (this.pluginOptions.enableStaticAnalysis) {
3357
+ try {
3358
+ const entrypoint = process.argv[1];
3359
+ console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3360
+ const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
3361
+ let staticSpec = await analyzer.analyze();
3362
+ if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
3363
+ deepMerge(this.pluginOptions.baseDocument, staticSpec);
3364
+ console.log("[ScalarPlugin] Static analysis completed successfully.");
3365
+ } catch (err) {
3366
+ console.error("[ScalarPlugin] Failed to run static analysis:", err);
3367
+ }
3368
+ }
3369
+ });
3370
+ }
3371
+ }
3372
+ }
3095
3373
  function Compression(options = {}) {
3096
3374
  const threshold = options.threshold ?? 512;
3375
+ const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
3097
3376
  const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
3098
3377
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
3099
3378
  let method = null;
@@ -3106,6 +3385,9 @@ function Compression(options = {}) {
3106
3385
  } else if (acceptEncoding.includes("gzip")) method = "gzip";
3107
3386
  else if (acceptEncoding.includes("deflate")) method = "deflate";
3108
3387
  if (!method) return next();
3388
+ if (!allowedAlgorithms.has(method)) {
3389
+ return next();
3390
+ }
3109
3391
  let response = await next();
3110
3392
  if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
3111
3393
  response = ctx._finalResponse;
@@ -3193,14 +3475,21 @@ function Cors(options = {}) {
3193
3475
  const origin = ctx.headers.get("origin");
3194
3476
  const set = (k, v) => headers.set(k, v);
3195
3477
  const append = (k, v) => headers.append(k, v);
3478
+ if (origin === "null" && opts.origin !== "null") {
3479
+ return next();
3480
+ }
3196
3481
  if (opts.origin === "*") {
3197
3482
  set("Access-Control-Allow-Origin", "*");
3198
3483
  } else if (typeof opts.origin === "string") {
3199
3484
  set("Access-Control-Allow-Origin", opts.origin);
3200
3485
  } else if (Array.isArray(opts.origin)) {
3201
- if (origin && opts.origin.includes(origin)) {
3202
- set("Access-Control-Allow-Origin", origin);
3203
- append("Vary", "Origin");
3486
+ if (origin) {
3487
+ const normalizedOrigin = origin.toLowerCase();
3488
+ const normalizedAllowed = opts.origin.map((o) => o.toLowerCase());
3489
+ if (normalizedAllowed.includes(normalizedOrigin)) {
3490
+ set("Access-Control-Allow-Origin", origin);
3491
+ append("Vary", "Origin");
3492
+ }
3204
3493
  }
3205
3494
  } else if (typeof opts.origin === "function") {
3206
3495
  const allowed = opts.origin(ctx);
@@ -3653,77 +3942,6 @@ function enableOpenApiValidation(app) {
3653
3942
  precompileValidators(app, spec);
3654
3943
  });
3655
3944
  }
3656
- const eta = new Eta();
3657
- class ScalarPlugin extends ShokupanRouter {
3658
- constructor(pluginOptions = {}) {
3659
- pluginOptions.config ??= {};
3660
- super();
3661
- this.pluginOptions = pluginOptions;
3662
- this.init();
3663
- }
3664
- init() {
3665
- this.get("/", (ctx) => {
3666
- let path = ctx.url.toString();
3667
- if (!path.endsWith("/")) path += "/";
3668
- return ctx.html(eta.renderString(`<!doctype html>
3669
- <html>
3670
- <head>
3671
- <title>API Reference</title>
3672
- <meta charset = "utf-8" />
3673
- <meta name="viewport" content = "width=device-width, initial-scale=1" />
3674
- </head>
3675
-
3676
- <body>
3677
- <div id="app"></div>
3678
- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
3679
- <script>
3680
- Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3681
- url: "<%= it.path %>openapi.json",
3682
- }
3683
- ])
3684
- <\/script>
3685
- </body>
3686
-
3687
- </html>`, { path, config: this.pluginOptions }));
3688
- });
3689
- this.get("/openapi.json", async (ctx) => {
3690
- let spec;
3691
- if (this.root.openApiSpec) {
3692
- try {
3693
- spec = structuredClone(this.root.openApiSpec);
3694
- } catch (e) {
3695
- spec = Object.assign({}, this.root.openApiSpec);
3696
- }
3697
- } else {
3698
- spec = await (this.root || this).generateApiSpec();
3699
- }
3700
- if (this.pluginOptions.baseDocument) {
3701
- deepMerge(spec, this.pluginOptions.baseDocument);
3702
- }
3703
- return ctx.json(spec);
3704
- });
3705
- }
3706
- // New lifecycle method to be called by router.mount
3707
- onMount(parent) {
3708
- if (parent.onStart) {
3709
- parent.onStart(async () => {
3710
- if (this.pluginOptions.enableStaticAnalysis) {
3711
- try {
3712
- const entrypoint = process.argv[1];
3713
- console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3714
- const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
3715
- let staticSpec = await analyzer.analyze();
3716
- if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
3717
- deepMerge(this.pluginOptions.baseDocument, staticSpec);
3718
- console.log("[ScalarPlugin] Static analysis completed successfully.");
3719
- } catch (err) {
3720
- console.error("[ScalarPlugin] Failed to run static analysis:", err);
3721
- }
3722
- }
3723
- });
3724
- }
3725
- }
3726
- }
3727
3945
  function SecurityHeaders(options = {}) {
3728
3946
  const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
3729
3947
  const headers = {};
@@ -3847,18 +4065,18 @@ class MemoryStore extends EventEmitter {
3847
4065
  }
3848
4066
  set(sid, sess, cb) {
3849
4067
  this.sessions[sid] = JSON.stringify(sess);
3850
- cb && cb();
4068
+ cb?.();
3851
4069
  }
3852
4070
  destroy(sid, cb) {
3853
4071
  delete this.sessions[sid];
3854
- cb && cb();
4072
+ cb?.();
3855
4073
  }
3856
4074
  touch(sid, sess, cb) {
3857
4075
  const current = this.sessions[sid];
3858
4076
  if (current) {
3859
4077
  this.sessions[sid] = JSON.stringify(sess);
3860
4078
  }
3861
- cb && cb();
4079
+ cb?.();
3862
4080
  }
3863
4081
  all(cb) {
3864
4082
  const result = {};
@@ -3874,7 +4092,7 @@ class MemoryStore extends EventEmitter {
3874
4092
  }
3875
4093
  clear(cb) {
3876
4094
  this.sessions = {};
3877
- cb && cb();
4095
+ cb?.();
3878
4096
  }
3879
4097
  }
3880
4098
  function sign(val, secret) {
@@ -3887,11 +4105,17 @@ function unsign(input, secret) {
3887
4105
  if (typeof secret !== "string") throw new TypeError("Secret string must be provided.");
3888
4106
  const tentValue = input.slice(0, input.lastIndexOf("."));
3889
4107
  const expectedInput = sign(tentValue, secret);
3890
- const expectedBuffer = Buffer.from(expectedInput);
3891
- const inputBuffer = Buffer.from(input);
3892
- if (expectedBuffer.length !== inputBuffer.length) return false;
3893
- const valid = require("crypto").timingSafeEqual(expectedBuffer, inputBuffer);
3894
- return valid ? tentValue : false;
4108
+ const maxLength = Math.max(expectedInput.length, input.length);
4109
+ const paddedExpected = Buffer.alloc(maxLength);
4110
+ const paddedInput = Buffer.alloc(maxLength);
4111
+ Buffer.from(expectedInput).copy(paddedExpected);
4112
+ Buffer.from(input).copy(paddedInput);
4113
+ try {
4114
+ const valid = require("crypto").timingSafeEqual(paddedExpected, paddedInput);
4115
+ return valid ? tentValue : false;
4116
+ } catch {
4117
+ return false;
4118
+ }
3895
4119
  }
3896
4120
  function Session(options) {
3897
4121
  const store = options.store || new MemoryStore();
@@ -4064,6 +4288,7 @@ export {
4064
4288
  All,
4065
4289
  AuthPlugin,
4066
4290
  Body,
4291
+ ClusterPlugin,
4067
4292
  Compression,
4068
4293
  Container,
4069
4294
  Controller,
@@ -4087,16 +4312,13 @@ export {
4087
4312
  RateLimitMiddleware,
4088
4313
  Req,
4089
4314
  RouteParamType,
4090
- RouterRegistry,
4091
4315
  ScalarPlugin,
4092
4316
  SecurityHeaders,
4093
4317
  Session,
4094
4318
  Shokupan,
4095
- ShokupanApplicationTree,
4096
4319
  ShokupanContext,
4097
4320
  ShokupanRequest,
4098
4321
  ShokupanResponse,
4099
- ShokupanRouter,
4100
4322
  Spec,
4101
4323
  Use,
4102
4324
  ValidationError,