stratal 0.0.18 → 0.0.20

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 (185) hide show
  1. package/README.md +8 -8
  2. package/dist/{base-email.provider-Cuw4OAB0.mjs → base-email.provider-CfQCA08m.mjs} +1 -1
  3. package/dist/{base-email.provider-Cuw4OAB0.mjs.map → base-email.provider-CfQCA08m.mjs.map} +1 -1
  4. package/dist/bin/cloudflare-workers-loader.mjs.map +1 -1
  5. package/dist/bin/quarry.mjs +26 -35
  6. package/dist/bin/quarry.mjs.map +1 -1
  7. package/dist/cache/index.d.mts +2 -153
  8. package/dist/cache/index.d.mts.map +1 -1
  9. package/dist/cache/index.mjs +4 -6
  10. package/dist/cache/index.mjs.map +1 -1
  11. package/dist/cache.service-DsnKuNyO.d.mts +156 -0
  12. package/dist/cache.service-DsnKuNyO.d.mts.map +1 -0
  13. package/dist/cache.tokens-B7Rw1C9Q.mjs +6 -0
  14. package/dist/cache.tokens-B7Rw1C9Q.mjs.map +1 -0
  15. package/dist/{colors-BTAnQRGU.mjs → colors-DJaRDXoS.mjs} +1 -1
  16. package/dist/{colors-BTAnQRGU.mjs.map → colors-DJaRDXoS.mjs.map} +1 -1
  17. package/dist/{command-DjGqCYHv.mjs → command-BgSlsS4M.mjs} +2 -2
  18. package/dist/{command-DjGqCYHv.mjs.map → command-BgSlsS4M.mjs.map} +1 -1
  19. package/dist/{command-B1YuV-UZ.d.mts → command-Bu-PjJrX.d.mts} +2 -2
  20. package/dist/{command-B1YuV-UZ.d.mts.map → command-Bu-PjJrX.d.mts.map} +1 -1
  21. package/dist/config/index.d.mts +81 -37
  22. package/dist/config/index.d.mts.map +1 -1
  23. package/dist/config/index.mjs +126 -45
  24. package/dist/config/index.mjs.map +1 -1
  25. package/dist/{consumer-registry-BkuHXR_u.d.mts → consumer-registry-B7yUNh0q.d.mts} +1 -1
  26. package/dist/{consumer-registry-BkuHXR_u.d.mts.map → consumer-registry-B7yUNh0q.d.mts.map} +1 -1
  27. package/dist/controller.decorator-DQzenvSN.mjs +66 -0
  28. package/dist/controller.decorator-DQzenvSN.mjs.map +1 -0
  29. package/dist/cron/index.d.mts +4 -3
  30. package/dist/cron/index.d.mts.map +1 -1
  31. package/dist/cron/index.mjs +1 -1
  32. package/dist/{cron-manager-1KnZvojs.mjs → cron-manager-7Symz_TE.mjs} +29 -19
  33. package/dist/cron-manager-7Symz_TE.mjs.map +1 -0
  34. package/dist/{cron-manager-BnEZquBL.d.mts → cron-manager-BEsH1mjW.d.mts} +27 -13
  35. package/dist/cron-manager-BEsH1mjW.d.mts.map +1 -0
  36. package/dist/di/index.d.mts +1 -1
  37. package/dist/di/index.mjs +2 -2
  38. package/dist/email/index.d.mts +3 -3
  39. package/dist/email/index.mjs +87 -10
  40. package/dist/email/index.mjs.map +1 -1
  41. package/dist/{en-3QnZwP-u.mjs → en-DSH_bhh6.mjs} +10 -30
  42. package/dist/en-DSH_bhh6.mjs.map +1 -0
  43. package/dist/env-D1rcZ8_r.d.mts +25 -0
  44. package/dist/env-D1rcZ8_r.d.mts.map +1 -0
  45. package/dist/errors/index.d.mts +1 -1
  46. package/dist/errors/index.mjs +1 -1
  47. package/dist/{errors--RBIvDXr.mjs → errors-BdyV5PnY.mjs} +180 -15
  48. package/dist/errors-BdyV5PnY.mjs.map +1 -0
  49. package/dist/{errors-B7hCnXgB.mjs → errors-Da3Pz2X7.mjs} +14 -7
  50. package/dist/errors-Da3Pz2X7.mjs.map +1 -0
  51. package/dist/events/index.d.mts +2 -2
  52. package/dist/events/index.mjs +1 -1
  53. package/dist/{events-UTJliZhl.mjs → events-COKixqnG.mjs} +2 -2
  54. package/dist/{events-UTJliZhl.mjs.map → events-COKixqnG.mjs.map} +1 -1
  55. package/dist/{gateway-context-BdBFoQd8.mjs → gateway-context-CdJjpUCW.mjs} +5 -70
  56. package/dist/gateway-context-CdJjpUCW.mjs.map +1 -0
  57. package/dist/guards/index.d.mts +14 -5
  58. package/dist/guards/index.d.mts.map +1 -1
  59. package/dist/guards/index.mjs +1 -1
  60. package/dist/{guards-MtDgcHnF.mjs → guards-DUk_Kzst.mjs} +1 -1
  61. package/dist/guards-DUk_Kzst.mjs.map +1 -0
  62. package/dist/http-method.decorator-DXwxAfb_.mjs +96 -0
  63. package/dist/http-method.decorator-DXwxAfb_.mjs.map +1 -0
  64. package/dist/i18n/index.d.mts +3 -3
  65. package/dist/i18n/index.mjs +2 -2
  66. package/dist/i18n/messages/en/index.d.mts +1 -1
  67. package/dist/i18n/messages/en/index.mjs +1 -1
  68. package/dist/i18n/utils/index.mjs +1 -1
  69. package/dist/i18n/validation/index.d.mts +2 -2
  70. package/dist/i18n/validation/index.mjs +2 -2
  71. package/dist/{i18n.module-BpLLLCTg.mjs → i18n.module-BBlNNlcG.mjs} +234 -204
  72. package/dist/i18n.module-BBlNNlcG.mjs.map +1 -0
  73. package/dist/index-7-hU3GTV.d.mts +101 -0
  74. package/dist/index-7-hU3GTV.d.mts.map +1 -0
  75. package/dist/{index-Dfpd_ypO.d.mts → index-Bnpfq6uk.d.mts} +81 -19
  76. package/dist/index-Bnpfq6uk.d.mts.map +1 -0
  77. package/dist/{index-BDh9J2KD.d.mts → index-C1KvMncZ.d.mts} +9 -29
  78. package/dist/{index-BDh9J2KD.d.mts.map → index-C1KvMncZ.d.mts.map} +1 -1
  79. package/dist/{index-DPxmo6AY.d.mts → index-CjaQ6_tZ.d.mts} +5 -4
  80. package/dist/index-CjaQ6_tZ.d.mts.map +1 -0
  81. package/dist/{index-BrmS34sa.d.mts → index-D0US0X14.d.mts} +375 -235
  82. package/dist/index-D0US0X14.d.mts.map +1 -0
  83. package/dist/{index-BR23zDMy.d.mts → index-DBd_2wv8.d.mts} +1 -1
  84. package/dist/{index-BR23zDMy.d.mts.map → index-DBd_2wv8.d.mts.map} +1 -1
  85. package/dist/index.d.mts +3 -2
  86. package/dist/index.d.mts.map +1 -1
  87. package/dist/index.mjs +1 -1
  88. package/dist/{is-command-PvULqiTa.mjs → is-command-C6a7WTPw.mjs} +2 -2
  89. package/dist/{is-command-PvULqiTa.mjs.map → is-command-C6a7WTPw.mjs.map} +1 -1
  90. package/dist/{is-seeder-BN9Ej1r7.mjs → is-seeder-CebjZCDn.mjs} +1 -1
  91. package/dist/{is-seeder-BN9Ej1r7.mjs.map → is-seeder-CebjZCDn.mjs.map} +1 -1
  92. package/dist/logger/index.d.mts +1 -1
  93. package/dist/logger/index.mjs +1 -1
  94. package/dist/{logger-c0ftIK4G.mjs → logger-V6Ms3QnQ.mjs} +38 -20
  95. package/dist/{logger-c0ftIK4G.mjs.map → logger-V6Ms3QnQ.mjs.map} +1 -1
  96. package/dist/macroable/index.d.mts +2 -0
  97. package/dist/macroable/index.mjs +2 -0
  98. package/dist/macroable-BmufBshB.mjs +122 -0
  99. package/dist/macroable-BmufBshB.mjs.map +1 -0
  100. package/dist/module/index.d.mts +2 -2
  101. package/dist/module/index.mjs +1 -1
  102. package/dist/{module-C3YZ-kZN.mjs → module-Dk2qTa77.mjs} +160 -19
  103. package/dist/module-Dk2qTa77.mjs.map +1 -0
  104. package/dist/openapi/index.d.mts +3 -3
  105. package/dist/openapi/index.mjs +2 -2
  106. package/dist/{openapi-tools.service-B77QXD56.mjs → openapi-tools.service-Zs-Ewv7F.mjs} +4 -1
  107. package/dist/{openapi-tools.service-B77QXD56.mjs.map → openapi-tools.service-Zs-Ewv7F.mjs.map} +1 -1
  108. package/dist/{openapi.service-6yj0BUY4.d.mts → openapi.service-BLgvn3hJ.d.mts} +3 -3
  109. package/dist/{openapi.service-6yj0BUY4.d.mts.map → openapi.service-BLgvn3hJ.d.mts.map} +1 -1
  110. package/dist/quarry/index.d.mts +7 -7
  111. package/dist/quarry/index.d.mts.map +1 -1
  112. package/dist/quarry/index.mjs +4 -4
  113. package/dist/{quarry-registry-CQCIlYTO.mjs → quarry-registry-DNEej-Db.mjs} +17 -15
  114. package/dist/quarry-registry-DNEej-Db.mjs.map +1 -0
  115. package/dist/queue/index.d.mts +2 -2
  116. package/dist/queue/index.mjs +2 -2
  117. package/dist/{queue.module-DIjD6nr-.mjs → queue.module-BCdCiySt.mjs} +4 -4
  118. package/dist/{queue.module-DIjD6nr-.mjs.map → queue.module-BCdCiySt.mjs.map} +1 -1
  119. package/dist/r2-storage.provider-Co6F0ZYV.mjs +244 -0
  120. package/dist/r2-storage.provider-Co6F0ZYV.mjs.map +1 -0
  121. package/dist/rate-limit.decorator--o6Q6p9w.mjs +55 -0
  122. package/dist/rate-limit.decorator--o6Q6p9w.mjs.map +1 -0
  123. package/dist/rate-limiter/index.d.mts +420 -0
  124. package/dist/rate-limiter/index.d.mts.map +1 -0
  125. package/dist/rate-limiter/index.mjs +365 -0
  126. package/dist/rate-limiter/index.mjs.map +1 -0
  127. package/dist/{resend.provider-Bvw36rQy.mjs → resend.provider-M6qRLrcy.mjs} +2 -2
  128. package/dist/{resend.provider-Bvw36rQy.mjs.map → resend.provider-M6qRLrcy.mjs.map} +1 -1
  129. package/dist/router/index.d.mts +2 -2
  130. package/dist/router/index.mjs +7 -5
  131. package/dist/seeder/index.d.mts +3 -3
  132. package/dist/seeder/index.mjs +2 -2
  133. package/dist/{seeder-D7VXULXB.mjs → seeder-CJAOHEIo.mjs} +5 -5
  134. package/dist/{seeder-D7VXULXB.mjs.map → seeder-CJAOHEIo.mjs.map} +1 -1
  135. package/dist/{setup-BRIN-iYT.mjs → setup-CefZKV_e.mjs} +1 -1
  136. package/dist/{setup-BRIN-iYT.mjs.map → setup-CefZKV_e.mjs.map} +1 -1
  137. package/dist/signed-url-BQPbv2In.mjs +74 -0
  138. package/dist/signed-url-BQPbv2In.mjs.map +1 -0
  139. package/dist/{smtp.provider-CAwpvzvD.mjs → smtp.provider-w0Ve52Xg.mjs} +2 -2
  140. package/dist/{smtp.provider-CAwpvzvD.mjs.map → smtp.provider-w0Ve52Xg.mjs.map} +1 -1
  141. package/dist/storage/index.d.mts +39 -17
  142. package/dist/storage/index.d.mts.map +1 -1
  143. package/dist/storage/index.mjs +3 -3
  144. package/dist/storage/providers/index.d.mts +30 -70
  145. package/dist/storage/providers/index.d.mts.map +1 -1
  146. package/dist/storage/providers/index.mjs +2 -2
  147. package/dist/{storage-CJ-QOwNv.mjs → storage-1zw-6Yiz.mjs} +101 -27
  148. package/dist/storage-1zw-6Yiz.mjs.map +1 -0
  149. package/dist/{storage-provider.interface-YRtyYBxV.d.mts → storage-provider.interface-Bd6vA4ak.d.mts} +20 -21
  150. package/dist/storage-provider.interface-Bd6vA4ak.d.mts.map +1 -0
  151. package/dist/{stratal-B7G4i9-N.mjs → stratal-DeEcGgdq.mjs} +57 -26
  152. package/dist/stratal-DeEcGgdq.mjs.map +1 -0
  153. package/dist/{types-CN0zONAZ.d.mts → types-cySNS_lp.d.mts} +1 -1
  154. package/dist/types-cySNS_lp.d.mts.map +1 -0
  155. package/dist/{usage-generator-Cl1HPlUp.mjs → usage-generator-BUdlhnCK.mjs} +2 -2
  156. package/dist/{usage-generator-Cl1HPlUp.mjs.map → usage-generator-BUdlhnCK.mjs.map} +1 -1
  157. package/dist/{validation-B4bePOa_.mjs → validation-DtJwAv7O.mjs} +62 -8
  158. package/dist/validation-DtJwAv7O.mjs.map +1 -0
  159. package/dist/websocket/index.d.mts +9 -4
  160. package/dist/websocket/index.d.mts.map +1 -1
  161. package/dist/websocket/index.mjs +1 -1
  162. package/dist/workers/index.d.mts +2 -1
  163. package/dist/workers/index.d.mts.map +1 -1
  164. package/dist/workers/index.mjs +2 -2
  165. package/package.json +32 -40
  166. package/dist/cron-manager-1KnZvojs.mjs.map +0 -1
  167. package/dist/cron-manager-BnEZquBL.d.mts.map +0 -1
  168. package/dist/en-3QnZwP-u.mjs.map +0 -1
  169. package/dist/errors--RBIvDXr.mjs.map +0 -1
  170. package/dist/errors-B7hCnXgB.mjs.map +0 -1
  171. package/dist/gateway-context-BdBFoQd8.mjs.map +0 -1
  172. package/dist/guards-MtDgcHnF.mjs.map +0 -1
  173. package/dist/i18n.module-BpLLLCTg.mjs.map +0 -1
  174. package/dist/index-BrmS34sa.d.mts.map +0 -1
  175. package/dist/index-DPxmo6AY.d.mts.map +0 -1
  176. package/dist/index-Dfpd_ypO.d.mts.map +0 -1
  177. package/dist/module-C3YZ-kZN.mjs.map +0 -1
  178. package/dist/quarry-registry-CQCIlYTO.mjs.map +0 -1
  179. package/dist/s3-storage.provider-BAhHDMI3.mjs +0 -343
  180. package/dist/s3-storage.provider-BAhHDMI3.mjs.map +0 -1
  181. package/dist/storage-CJ-QOwNv.mjs.map +0 -1
  182. package/dist/storage-provider.interface-YRtyYBxV.d.mts.map +0 -1
  183. package/dist/stratal-B7G4i9-N.mjs.map +0 -1
  184. package/dist/types-CN0zONAZ.d.mts.map +0 -1
  185. package/dist/validation-B4bePOa_.mjs.map +0 -1
@@ -1,12 +1,16 @@
1
- import { A as Scope, D as runWithContainer, E as getContainer, H as ApplicationError, V as ROUTER_TOKENS, a as createHttpExceptionContext, c as DEFAULT_CONTENT_TYPE, d as ROUTER_CONTEXT_KEYS, f as ROUTE_METADATA_KEYS, k as ERROR_CODES, l as HTTP_METHODS, m as VERSION_NEUTRAL, p as SECURITY_SCHEMES, s as RouterContext, u as METHOD_STATUS_CODES, w as I18N_TOKENS } from "./errors--RBIvDXr.mjs";
2
- import { a as __decorate, d as CONTAINER_TOKEN, f as DI_TOKENS, g as getMethodInjections, o as __decorateParam, p as Transient, s as __decorateMetadata, u as LOGGER_TOKENS } from "./logger-c0ftIK4G.mjs";
3
- import { S as Module, _ as OpenAPIRouteRegistrationError, b as ControllerMethodNotFoundError, c as InvalidSignatureError, d as ResponseValidationError, f as RouteNameNotFoundError, h as RouteNotFoundError, l as MissingEnvironmentVariableError, m as SchemaValidationError, o as DomainMismatchError, s as DuplicateRouteNameError, u as MissingRouteParamError, v as HonoAppAlreadyConfiguredError, y as ControllerRegistrationError } from "./module-C3YZ-kZN.mjs";
4
- import { i as z, o as backendErrorMap, r as validation_exports, s as runWithErrorMapContext, t as OpenAPIHono } from "./validation-B4bePOa_.mjs";
5
- import { n as OPENAPI_TOKENS } from "./openapi-tools.service-B77QXD56.mjs";
6
- import { t as en_exports } from "./en-3QnZwP-u.mjs";
7
- import { i as getMethodGuards, r as getControllerGuards, t as GuardExecutionService } from "./guards-MtDgcHnF.mjs";
8
- import { c as getWsOnMessageMethod, d as isGateway, m as getControllerRoute, o as getWsOnCloseMethod, p as getControllerOptions, s as getWsOnErrorMethod, t as GatewayContext } from "./gateway-context-BdBFoQd8.mjs";
9
- import { t as setupI18nCompiler } from "./setup-BRIN-iYT.mjs";
1
+ import { A as Scope, D as runWithContainer, E as getContainer, H as ApplicationError, V as ROUTER_TOKENS, a as createHttpExceptionContext, c as DEFAULT_CONTENT_TYPE, d as ROUTER_CONTEXT_KEYS, f as ROUTE_METADATA_KEYS, k as ERROR_CODES, l as HTTP_METHODS, m as VERSION_NEUTRAL, p as SECURITY_SCHEMES, s as RouterContext, u as METHOD_STATUS_CODES, w as I18N_TOKENS } from "./errors-BdyV5PnY.mjs";
2
+ import { a as __decorate, d as CONTAINER_TOKEN, f as DI_TOKENS, g as getMethodInjections, o as __decorateParam, p as Transient, s as __decorateMetadata, u as LOGGER_TOKENS } from "./logger-V6Ms3QnQ.mjs";
3
+ import { S as createThrottleMiddleware, b as ControllerRegistrationError, c as InvalidSignatureError, d as MissingRouteParamError, f as ResponseValidationError, g as RouteNotFoundError, h as SchemaValidationError, k as Module, l as MiddlewareNextCalledMultipleTimesError, o as DomainMismatchError, p as RouteNameNotFoundError, s as DuplicateRouteNameError, u as MissingEnvironmentVariableError, v as OpenAPIRouteRegistrationError, x as ControllerMethodNotFoundError, y as HonoAppAlreadyConfiguredError } from "./module-Dk2qTa77.mjs";
4
+ import { c as backendErrorMap, i as z, l as runWithErrorMapContext, r as validation_exports, t as OpenAPIHono } from "./validation-DtJwAv7O.mjs";
5
+ import { n as OPENAPI_TOKENS } from "./openapi-tools.service-Zs-Ewv7F.mjs";
6
+ import { t as en_exports } from "./en-DSH_bhh6.mjs";
7
+ import { i as getMethodGuards, r as getControllerGuards, t as GuardExecutionService } from "./guards-DUk_Kzst.mjs";
8
+ import { n as getControllerOptions, r as getControllerRoute } from "./controller.decorator-DQzenvSN.mjs";
9
+ import { c as getWsOnMessageMethod, d as isGateway, o as getWsOnCloseMethod, s as getWsOnErrorMethod, t as GatewayContext } from "./gateway-context-CdJjpUCW.mjs";
10
+ import "./http-method.decorator-DXwxAfb_.mjs";
11
+ import { n as getRateLimits } from "./rate-limit.decorator--o6Q6p9w.mjs";
12
+ import { n as verifySignedUrl, t as signUrl } from "./signed-url-BQPbv2In.mjs";
13
+ import { t as setupI18nCompiler } from "./setup-CefZKV_e.mjs";
10
14
  import { inject } from "tsyringe";
11
15
  import { createCoreContext, translate } from "@intlify/core-base";
12
16
  import { swaggerUI } from "@hono/swagger-ui";
@@ -705,104 +709,88 @@ function createMiddlewareChain(classes) {
705
709
  let current = next;
706
710
  for (let i = classes.length - 1; i >= 0; i--) {
707
711
  const prevNext = current;
708
- const middleware = requestContainer.resolve(classes[i]);
709
- current = () => middleware.handle(ctx, prevNext);
712
+ const middlewareClass = classes[i];
713
+ current = () => {
714
+ const middleware = requestContainer.resolve(middlewareClass);
715
+ let called = false;
716
+ const guardedNext = () => {
717
+ if (called) {
718
+ const err = new MiddlewareNextCalledMultipleTimesError(middlewareClass.name ?? "anonymous");
719
+ console.error("[STRATAL DEBUG] next() called multiple times for " + middlewareClass.name);
720
+ console.error("[STRATAL DEBUG] Stack trace:", (/* @__PURE__ */ new Error()).stack);
721
+ return Promise.reject(err);
722
+ }
723
+ called = true;
724
+ return prevNext();
725
+ };
726
+ return middleware.handle(ctx, guardedNext);
727
+ };
710
728
  }
711
729
  const result = await current();
712
730
  if (result instanceof Response) return result;
713
731
  };
714
732
  }
715
733
  //#endregion
716
- //#region src/router/decorators/http-method.decorator.ts
717
- /**
718
- * Creates an HTTP method decorator factory for the given HTTP method.
719
- *
720
- * The returned decorator stores {@link ExplicitRouteMetadata} on the method and
721
- * tracks the method name under {@link ROUTE_METADATA_KEYS.DECORATED_METHODS}
722
- * on the controller prototype so they can be discovered at registration time.
723
- */
724
- function createHttpMethodDecorator(method) {
725
- return function(path, config) {
726
- return function(target, propertyKey, descriptor) {
727
- const metadata = {
728
- type: "explicit",
729
- method,
730
- path,
731
- config: config ?? { response: z.any() }
732
- };
733
- Reflect.defineMetadata(ROUTE_METADATA_KEYS.ROUTE_CONFIG, metadata, target, propertyKey);
734
- const existing = Reflect.getOwnMetadata(ROUTE_METADATA_KEYS.DECORATED_METHODS, target) ?? [];
735
- existing.push(propertyKey);
736
- Reflect.defineMetadata(ROUTE_METADATA_KEYS.DECORATED_METHODS, existing, target);
737
- return descriptor;
738
- };
739
- };
740
- }
734
+ //#region src/router/trailing-slash.ts
741
735
  /**
742
- * Registers a GET route on the controller method.
743
- *
744
- * @param path - Route path relative to the controller base path
745
- * @param config - Optional route configuration (response schema, body, params, etc.)
746
- *
747
- * @example
748
- * ```typescript
749
- * @Controller('/api/v1/users')
750
- * class UsersController {
751
- * @Get('/', { response: z.array(userSchema), summary: 'List users' })
752
- * async list(ctx: RouterContext) { ... }
736
+ * Apply a trailing-slash mode to a URL or path.
753
737
  *
754
- * @Get('/:id', { params: z.object({ id: z.string().uuid() }), response: userSchema })
755
- * async getUser(ctx: RouterContext) { ... }
756
- * }
757
- * ```
758
- */
759
- const Get = createHttpMethodDecorator("get");
760
- /**
761
- * Registers a POST route on the controller method.
738
+ * - `'ignore'` return as-is.
739
+ * - `'always'` append `/` to the pathname unless it already has one.
740
+ * Skipped when the last segment contains `.` (file-like paths) and for the
741
+ * root `/` path.
742
+ * - `'never'` — strip a trailing `/` from the pathname. Skipped for root.
762
743
  *
763
- * @param path - Route path relative to the controller base path
764
- * @param config - Optional route configuration (response schema, body, params, etc.)
744
+ * Preserves query string and hash. Handles both relative paths
745
+ * (`/foo?x=1`) and absolute URLs (`https://host/foo?x=1`).
765
746
  *
766
- * @example
767
- * ```typescript
768
- * @Controller('/api/v1/users')
769
- * class UsersController {
770
- * @Post('/', { body: createUserSchema, response: userSchema, statusCode: 201 })
771
- * async createUser(ctx: RouterContext) { ... }
772
- * }
773
- * ```
747
+ * Used by URL-generation helpers and the redirect middleware so canonical
748
+ * form is computed in one place.
774
749
  */
775
- const Post = createHttpMethodDecorator("post");
750
+ function applyTrailingSlash(url, mode) {
751
+ if (mode === "ignore") return url;
752
+ const isAbsolute = /^https?:\/\//i.test(url);
753
+ const parsed = isAbsolute ? new URL(url) : new URL(url, "http://placeholder.local");
754
+ const path = parsed.pathname;
755
+ if (path === "/") return url;
756
+ const hasTrailing = path.endsWith("/");
757
+ if (mode === "always" && !hasTrailing) {
758
+ if (path.slice(path.lastIndexOf("/") + 1).includes(".")) return url;
759
+ parsed.pathname = `${path}/`;
760
+ } else if (mode === "never" && hasTrailing) parsed.pathname = path.slice(0, -1);
761
+ else return url;
762
+ return isAbsolute ? parsed.toString() : `${parsed.pathname}${parsed.search}${parsed.hash}`;
763
+ }
764
+ //#endregion
765
+ //#region src/router/middleware/trailing-slash-redirect.ts
766
+ const REDIRECT_STATUS = 308;
776
767
  /**
777
- * Registers a PUT route on the controller method.
768
+ * Create a Hono middleware that canonicalises trailing slashes via 308 redirects.
778
769
  *
779
- * @param path - Route path relative to the controller base path
780
- * @param config - Optional route configuration
781
- */
782
- const Put = createHttpMethodDecorator("put");
783
- /**
784
- * Registers a PATCH route on the controller method.
770
+ * - `'ignore'` returns `null`; routes match both `/foo` and `/foo/` natively
771
+ * (Hono handles this when constructed with `strict: false`).
772
+ * - `'always'` — non-trailing requests redirect to the trailing-slash form.
773
+ * Paths whose last segment contains `.` (e.g. `/api/openapi.json`) are skipped.
774
+ * - `'never'` — trailing requests redirect to the non-trailing form.
785
775
  *
786
- * @param path - Route path relative to the controller base path
787
- * @param config - Optional route configuration
788
- */
789
- const Patch = createHttpMethodDecorator("patch");
790
- /**
791
- * Registers a DELETE route on the controller method.
776
+ * Root (`/`) is always passed through unchanged.
792
777
  *
793
- * @param path - Route path relative to the controller base path
794
- * @param config - Optional route configuration
795
- */
796
- const Delete = createHttpMethodDecorator("delete");
797
- /**
798
- * Registers an ALL (any HTTP method) route on the controller method.
799
- * Routes using @All are registered without OpenAPI validation
800
- * since OpenAPI does not support a catch-all HTTP method.
778
+ * 308 is used so that POST/PUT/PATCH bodies survive the redirect.
801
779
  *
802
- * @param path - Route path relative to the controller base path
803
- * @param config - Optional route configuration
780
+ * Location headers are emitted as path-relative URIs so the user agent
781
+ * resolves them against the effective request URI — sidestepping scheme
782
+ * mismatches behind HTTPS-terminating proxies that proxy HTTPS pages to an
783
+ * HTTP-speaking backend (which would otherwise produce a mixed-content block).
804
784
  */
805
- const All = createHttpMethodDecorator("all");
785
+ function createTrailingSlashRedirect(mode) {
786
+ if (mode === "ignore") return null;
787
+ return async (c, next) => {
788
+ const url = new URL(c.req.url);
789
+ const canonicalPath = applyTrailingSlash(url.pathname, mode);
790
+ if (canonicalPath === url.pathname) return next();
791
+ return c.redirect(`${canonicalPath}${url.search}`, REDIRECT_STATUS);
792
+ };
793
+ }
806
794
  //#endregion
807
795
  //#region src/router/decorators/route.decorator.ts
808
796
  /**
@@ -1212,10 +1200,12 @@ let RouteRegistrationService = class RouteRegistrationService {
1212
1200
  const controllerOpts = getControllerOptions(ControllerClass);
1213
1201
  const controllerGuards = getControllerGuards(ControllerClass)?.guards ?? [];
1214
1202
  const routerConfig = this.routerResolver?.resolveForController(ControllerClass) ?? { middleware: [] };
1203
+ const classThrottleMiddleware = Array.from(new Set(getRateLimits(ControllerClass).map(createThrottleMiddleware)));
1215
1204
  const basePath = routerConfig.prefix ? this.joinPaths(routerConfig.prefix, controllerRoute) : controllerRoute;
1216
1205
  const effectiveVersion = controllerOpts?.version ?? routerConfig.version;
1217
1206
  const effectiveDomain = controllerOpts?.domain ?? routerConfig.domain;
1218
1207
  if (isWsGateway) {
1208
+ const wsMiddleware = [...routerConfig.middleware, ...classThrottleMiddleware];
1219
1209
  const expandedRoutes = this.registry.register({
1220
1210
  method: "ws",
1221
1211
  basePath,
@@ -1224,10 +1214,10 @@ let RouteRegistrationService = class RouteRegistrationService {
1224
1214
  controller: ControllerClass.name,
1225
1215
  action: "ws",
1226
1216
  hidden: routerConfig.hideFromDocs ?? false,
1227
- middleware: routerConfig.middleware.map((m) => m.name)
1217
+ middleware: wsMiddleware.map((m) => m.name)
1228
1218
  });
1229
1219
  for (const route of expandedRoutes) actions.set(route, () => {
1230
- if (routerConfig.middleware.length > 0) this.app.use(`${route.path}/*`, createMiddlewareChain(routerConfig.middleware));
1220
+ if (wsMiddleware.length > 0) this.app.use(route.path, createMiddlewareChain(wsMiddleware));
1231
1221
  if (effectiveDomain) {
1232
1222
  const domainHandler = createDomainMiddleware(effectiveDomain);
1233
1223
  this.app.use(route.path, domainHandler);
@@ -1241,6 +1231,7 @@ let RouteRegistrationService = class RouteRegistrationService {
1241
1231
  this.controllerClasses.set(className, ControllerClass);
1242
1232
  const prototype = ControllerClass.prototype;
1243
1233
  if (prototype.handle) {
1234
+ const wildcardMiddleware = [...routerConfig.middleware, ...classThrottleMiddleware];
1244
1235
  const expandedRoutes = this.registry.register({
1245
1236
  method: "all",
1246
1237
  basePath,
@@ -1249,10 +1240,10 @@ let RouteRegistrationService = class RouteRegistrationService {
1249
1240
  controller: className,
1250
1241
  action: "handle",
1251
1242
  hidden: routerConfig.hideFromDocs ?? false,
1252
- middleware: routerConfig.middleware.map((m) => m.name)
1243
+ middleware: wildcardMiddleware.map((m) => m.name)
1253
1244
  });
1254
1245
  for (const route of expandedRoutes) actions.set(route, () => {
1255
- if (routerConfig.middleware.length > 0) this.app.use(`${route.path}/*`, createMiddlewareChain(routerConfig.middleware));
1246
+ if (wildcardMiddleware.length > 0) this.app.use(route.path, createMiddlewareChain(wildcardMiddleware));
1256
1247
  this.registerWildcardRoute(ControllerClass, route.path);
1257
1248
  });
1258
1249
  return;
@@ -1273,16 +1264,31 @@ let RouteRegistrationService = class RouteRegistrationService {
1273
1264
  else if (meta.type === "explicit") hasExplicit = true;
1274
1265
  }
1275
1266
  if (hasConvention && hasExplicit) throw new ControllerRegistrationError(ControllerClass.name, "Cannot mix @Route() with HTTP method decorators (@Get, @Post, etc.) in the same controller. Use one pattern or the other.");
1276
- let scopedMiddlewareApplied = false;
1277
1267
  const routerHidden = routerConfig.hideFromDocs;
1278
1268
  const controllerHidden = controllerOpts?.hideFromDocs ?? false;
1279
- const effectiveNamePrefix = controllerOpts?.name ?? routerConfig.name;
1280
- const middlewareNames = routerConfig.middleware.map((m) => m.name);
1269
+ const routerName = routerConfig.name;
1270
+ const controllerName = controllerOpts?.name;
1271
+ const effectiveNamePrefix = routerName && controllerName ? `${routerName}${controllerName}` : routerName ?? controllerName;
1281
1272
  for (const { method: methodName, meta } of methodMetadata) {
1282
1273
  const resolved = this.resolveMethodAndPath(meta, methodName, basePath, className);
1283
1274
  if (!resolved) continue;
1284
- const { httpMethod, fullPath, routeConfig, statusCodeOverride } = resolved;
1285
- if (routerConfig.params) routeConfig.params = routeConfig.params ? routerConfig.params.extend(routeConfig.params.shape) : routerConfig.params;
1275
+ const methodThrottleMiddleware = getRateLimits(prototype, methodName).map(createThrottleMiddleware);
1276
+ const effectiveMiddleware = Array.from(new Set([
1277
+ ...routerConfig.middleware,
1278
+ ...classThrottleMiddleware,
1279
+ ...methodThrottleMiddleware
1280
+ ]));
1281
+ const middlewareNames = effectiveMiddleware.map((m) => m.name);
1282
+ const { httpMethod, fullPath, routeConfig: rawRouteConfig, statusCodeOverride } = resolved;
1283
+ let mergedParams = rawRouteConfig.params;
1284
+ if (routerConfig.params) {
1285
+ const prefixShape = routerConfig.params.shape;
1286
+ mergedParams = mergedParams ? mergedParams.extend(prefixShape) : routerConfig.params.extend({});
1287
+ }
1288
+ const routeConfig = mergedParams === rawRouteConfig.params ? rawRouteConfig : {
1289
+ ...rawRouteConfig,
1290
+ params: mergedParams
1291
+ };
1286
1292
  const hideFromDocs = routeConfig.hideFromDocs ?? routerHidden ?? controllerHidden;
1287
1293
  let routeName;
1288
1294
  if (routeConfig.name) routeName = effectiveNamePrefix ? `${effectiveNamePrefix}${routeConfig.name}` : routeConfig.name;
@@ -1306,11 +1312,6 @@ let RouteRegistrationService = class RouteRegistrationService {
1306
1312
  const responseSchema = httpMethod !== "all" ? this.extractResponseSchema(routeConfig) : null;
1307
1313
  const handler = this.createControllerHandler(ControllerClass, methodName, responseSchema);
1308
1314
  for (const route of expandedRoutes) actions.set(route, () => {
1309
- if (!scopedMiddlewareApplied && routerConfig.middleware.length > 0) {
1310
- const primaryRoute = expandedRoutes.find((r) => !r.isLocaleVariant) ?? expandedRoutes[0];
1311
- this.app.use(`${primaryRoute.path}/*`, createMiddlewareChain(routerConfig.middleware));
1312
- scopedMiddlewareApplied = true;
1313
- }
1314
1315
  if (effectiveDomain) {
1315
1316
  const domainHandler = createDomainMiddleware(effectiveDomain);
1316
1317
  this.app.use(route.path, domainHandler);
@@ -1329,12 +1330,13 @@ let RouteRegistrationService = class RouteRegistrationService {
1329
1330
  path: route.path,
1330
1331
  methodName
1331
1332
  });
1332
- if (allGuards.length > 0) this.app.all(route.path, this.createGuardMiddleware(allGuards), handler);
1333
- else this.app.all(route.path, handler);
1333
+ if (effectiveMiddleware.length > 0) this.app.use(route.path, createMiddlewareChain(effectiveMiddleware));
1334
+ if (allGuards.length > 0) this.app.use(route.path, this.createGuardMiddleware(allGuards));
1335
+ this.app.all(route.path, handler);
1334
1336
  return;
1335
1337
  }
1336
1338
  const metadata = this.mergeMetadata(controllerOpts, routeConfig, ControllerClass, methodName);
1337
- const openApiRoute = this.buildOpenAPIRoute(httpMethod, route.path, routeConfig, metadata, allGuards, meta.type === "convention" ? methodName : void 0, statusCodeOverride, route.isLocaleVariant ?? false);
1339
+ const openApiRoute = this.buildOpenAPIRoute(httpMethod, route.path, routeConfig, metadata, meta.type === "convention" ? methodName : void 0, statusCodeOverride, route.isLocaleVariant ?? false);
1338
1340
  this.logger.info(`Registering route`, {
1339
1341
  controller: className,
1340
1342
  method: httpMethod.toUpperCase(),
@@ -1343,7 +1345,8 @@ let RouteRegistrationService = class RouteRegistrationService {
1343
1345
  tags: metadata.tags,
1344
1346
  hidden: route.hidden
1345
1347
  });
1346
- this.app.openapi(openApiRoute, handler);
1348
+ const wrappedHandler = this.wrapHandlerWithChain(handler, effectiveMiddleware, allGuards);
1349
+ this.app.openapi(openApiRoute, wrappedHandler);
1347
1350
  if (!route.hidden) {
1348
1351
  const { hide: _, ...specRoute } = openApiRoute;
1349
1352
  this.app.openAPIRegistry.registerPath({
@@ -1416,6 +1419,31 @@ let RouteRegistrationService = class RouteRegistrationService {
1416
1419
  };
1417
1420
  }
1418
1421
  /**
1422
+ * Wrap a controller handler with a `scopedMiddleware → guards → handler`
1423
+ * chain that runs *inside* the Hono route handler — after request
1424
+ * validators have populated `c.req.valid(...)`. This is the only place
1425
+ * we can run user middleware after `@hono/zod-openapi`'s validators in
1426
+ * the same pipeline.
1427
+ *
1428
+ * Returns a Hono handler with the same signature as the original so
1429
+ * `app.openapi(route, wrapped)` works transparently.
1430
+ */
1431
+ wrapHandlerWithChain(handler, scopedMiddleware, guards) {
1432
+ if (scopedMiddleware.length === 0 && guards.length === 0) return handler;
1433
+ const scopedChain = scopedMiddleware.length > 0 ? createMiddlewareChain(scopedMiddleware) : null;
1434
+ const guardChain = guards.length > 0 ? this.createGuardMiddleware(guards) : null;
1435
+ return async (c) => {
1436
+ let captured;
1437
+ const runHandler = async () => {
1438
+ captured = await handler(c);
1439
+ };
1440
+ const runGuards = guardChain ? () => guardChain(c, runHandler) : runHandler;
1441
+ const result = await (scopedChain ? () => scopedChain(c, runGuards) : runGuards)();
1442
+ if (result instanceof Response) return result;
1443
+ return captured;
1444
+ };
1445
+ }
1446
+ /**
1419
1447
  * Register wildcard route for non-RESTful controllers
1420
1448
  */
1421
1449
  registerWildcardRoute(ControllerClass, route) {
@@ -1453,9 +1481,9 @@ let RouteRegistrationService = class RouteRegistrationService {
1453
1481
  * Join a base path and a route path, normalizing slashes
1454
1482
  */
1455
1483
  joinPaths(basePath, routePath) {
1456
- if (routePath === "/") return basePath;
1457
- if (basePath !== "/" && basePath.endsWith("/")) basePath = basePath.slice(0, -1);
1458
- if (routePath && !routePath.startsWith("/")) routePath = "/" + routePath;
1484
+ if (basePath.endsWith("/")) basePath = basePath.slice(0, -1);
1485
+ if (routePath === "/" || routePath === "") return basePath || "/";
1486
+ if (!routePath.startsWith("/")) routePath = "/" + routePath;
1459
1487
  return basePath + routePath;
1460
1488
  }
1461
1489
  /**
@@ -1492,12 +1520,13 @@ let RouteRegistrationService = class RouteRegistrationService {
1492
1520
  }
1493
1521
  /**
1494
1522
  * Build OpenAPI route configuration from metadata
1495
- * Creates a route definition compatible with @hono/zod-openapi
1496
- * Includes guard execution for proper access control
1523
+ * Creates a route definition compatible with @hono/zod-openapi.
1497
1524
  *
1498
- * Execution order: Global middlewares Guards Handler
1525
+ * Scoped middleware and guards are NOT attached to `route.middleware`
1526
+ * here — they're composed into a wrapped handler in `collectRoutes` so
1527
+ * they run after Hono's request validators. See `wrapHandlerWithChain`.
1499
1528
  */
1500
- buildOpenAPIRoute(method, path, routeConfig, metadata, guards, methodName, statusCodeOverride, hasLocaleParam = false) {
1529
+ buildOpenAPIRoute(method, path, routeConfig, metadata, methodName, statusCodeOverride, hasLocaleParam = false) {
1501
1530
  try {
1502
1531
  const route = {
1503
1532
  method,
@@ -1506,7 +1535,6 @@ let RouteRegistrationService = class RouteRegistrationService {
1506
1535
  responses: {},
1507
1536
  hide: true
1508
1537
  };
1509
- if (guards.length > 0) route.middleware = [this.createGuardMiddleware(guards)];
1510
1538
  if (routeConfig.body) {
1511
1539
  const bodySchema = this.isRouteBodyObject(routeConfig.body) ? routeConfig.body.schema : routeConfig.body;
1512
1540
  const bodyContentType = this.isRouteBodyObject(routeConfig.body) ? routeConfig.body.contentType ?? "application/json" : DEFAULT_CONTENT_TYPE;
@@ -1680,12 +1708,16 @@ let HonoApp = class HonoApp extends OpenAPIHono {
1680
1708
  * Used by private methods to register middleware without going through the override.
1681
1709
  */
1682
1710
  nativeUse;
1683
- constructor(container, logger) {
1684
- super({ defaultHook: (result, c) => {
1685
- if (!result.success) throw new SchemaValidationError(result.error);
1686
- const override = c.get("validationSuccessResponse");
1687
- if (override) return override;
1688
- } });
1711
+ constructor(container, logger, application) {
1712
+ const trailingSlash = application.config.trailingSlash ?? "ignore";
1713
+ super({
1714
+ strict: false,
1715
+ defaultHook: (result, c) => {
1716
+ if (!result.success) throw new SchemaValidationError(result.error);
1717
+ const override = c.get("validationSuccessResponse");
1718
+ if (override) return override;
1719
+ }
1720
+ });
1689
1721
  this._container = container;
1690
1722
  this._logger = logger;
1691
1723
  this.nativeUse = this.use;
@@ -1700,6 +1732,8 @@ let HonoApp = class HonoApp extends OpenAPIHono {
1700
1732
  }
1701
1733
  return this.nativeUse(...args);
1702
1734
  });
1735
+ const trailingSlashRedirect = createTrailingSlashRedirect(trailingSlash);
1736
+ if (trailingSlashRedirect) this.nativeUse("*", trailingSlashRedirect);
1703
1737
  this.setupRequestScope();
1704
1738
  this.applyGlobalMiddleware();
1705
1739
  }
@@ -1742,7 +1776,12 @@ HonoApp = __decorate([
1742
1776
  Transient(),
1743
1777
  __decorateParam(0, inject(CONTAINER_TOKEN)),
1744
1778
  __decorateParam(1, inject(LOGGER_TOKENS.LoggerService)),
1745
- __decorateMetadata("design:paramtypes", [Object, Object])
1779
+ __decorateParam(2, inject(DI_TOKENS.Application)),
1780
+ __decorateMetadata("design:paramtypes", [
1781
+ Object,
1782
+ Object,
1783
+ Object
1784
+ ])
1746
1785
  ], HonoApp);
1747
1786
  //#endregion
1748
1787
  //#region src/router/services/locale-path.service.ts
@@ -1891,10 +1930,21 @@ VersioningService = __decorate([
1891
1930
  ], VersioningService);
1892
1931
  //#endregion
1893
1932
  //#region src/router/route-registry.ts
1933
+ const CONCRETE_HTTP_METHODS = [
1934
+ "get",
1935
+ "post",
1936
+ "put",
1937
+ "delete",
1938
+ "patch",
1939
+ "head",
1940
+ "options",
1941
+ "trace"
1942
+ ];
1894
1943
  let RouteRegistry = class RouteRegistry {
1895
1944
  routes = [];
1896
1945
  namedRoutes = /* @__PURE__ */ new Map();
1897
1946
  _sortedCache = null;
1947
+ _routeToNameCache = null;
1898
1948
  constructor(versioningService, localePathService) {
1899
1949
  this.versioningService = versioningService;
1900
1950
  this.localePathService = localePathService;
@@ -1945,6 +1995,7 @@ let RouteRegistry = class RouteRegistry {
1945
1995
  }
1946
1996
  }
1947
1997
  this._sortedCache = null;
1998
+ this._routeToNameCache = null;
1948
1999
  return expandedRoutes;
1949
2000
  }
1950
2001
  /** Get a named route by name */
@@ -1955,6 +2006,25 @@ let RouteRegistry = class RouteRegistry {
1955
2006
  has(name) {
1956
2007
  return this.namedRoutes.has(name);
1957
2008
  }
2009
+ /**
2010
+ * Resolve a Hono-style route path pattern (e.g. as exposed by `c.req.routePath`)
2011
+ * back to its registered name, scoped to the request's HTTP method. Locale variant
2012
+ * paths resolve to the canonical primary route name. Method matching is
2013
+ * case-insensitive; routes registered with `'all'` resolve under any verb.
2014
+ */
2015
+ findNameByRoute(method, path) {
2016
+ this._routeToNameCache ??= this.buildRouteToNameCache();
2017
+ return this._routeToNameCache.get(`${method.toLowerCase()}:${path}`);
2018
+ }
2019
+ buildRouteToNameCache() {
2020
+ const cache = /* @__PURE__ */ new Map();
2021
+ for (const route of this.namedRoutes.values()) {
2022
+ const methods = route.method === "all" ? CONCRETE_HTTP_METHODS : [route.method];
2023
+ const paths = route.localePaths ? [route.path, ...route.localePaths] : [route.path];
2024
+ for (const m of methods) for (const p of paths) cache.set(`${m}:${p}`, route.name);
2025
+ }
2026
+ return cache;
2027
+ }
1958
2028
  /** Get all routes sorted by specificity (static > param > wildcard, primary before locale) */
1959
2029
  all() {
1960
2030
  this._sortedCache ??= sortRoutesBySpecificity(this.routes);
@@ -1972,78 +2042,18 @@ RouteRegistry = __decorate([
1972
2042
  __decorateMetadata("design:paramtypes", [Object, Object])
1973
2043
  ], RouteRegistry);
1974
2044
  //#endregion
1975
- //#region src/router/signed-url.ts
1976
- /**
1977
- * Import a signing key for HMAC-SHA256.
1978
- */
1979
- async function importKey(secret) {
1980
- const encoder = new TextEncoder();
1981
- return crypto.subtle.importKey("raw", encoder.encode(secret), {
1982
- name: "HMAC",
1983
- hash: "SHA-256"
1984
- }, false, ["sign", "verify"]);
1985
- }
1986
- /**
1987
- * Encode an ArrayBuffer as base64url (URL-safe base64).
1988
- */
1989
- function toBase64Url(buffer) {
1990
- const bytes = new Uint8Array(buffer);
1991
- let binary = "";
1992
- for (const byte of bytes) binary += String.fromCharCode(byte);
1993
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1994
- }
1995
- /**
1996
- * Sign a URL with HMAC-SHA256.
1997
- *
1998
- * Appends `signature` and optionally `expires` query parameters.
1999
- * The signature covers the pathname + search (excluding the signature params themselves).
2000
- *
2001
- * @param url - Full URL or path to sign
2002
- * @param secret - HMAC secret key (e.g., from env.APP_SECRET)
2003
- * @param options - Optional expiration
2004
- * @returns URL string with `signature` (and `expires`) query params appended
2005
- */
2006
- async function signUrl(url, secret, options) {
2007
- const parsedUrl = new URL(url, "https://placeholder.local");
2008
- const key = await importKey(secret);
2009
- if (options?.expiresIn) {
2010
- const expires = Math.floor(Date.now() / 1e3) + options.expiresIn;
2011
- parsedUrl.searchParams.set("expires", String(expires));
2012
- }
2013
- const dataToSign = `${parsedUrl.pathname}?${parsedUrl.searchParams.toString()}`;
2014
- const encoder = new TextEncoder();
2015
- const signature = toBase64Url(await crypto.subtle.sign("HMAC", key, encoder.encode(dataToSign)));
2016
- parsedUrl.searchParams.set("signature", signature);
2017
- return url.startsWith("http") ? parsedUrl.toString() : `${parsedUrl.pathname}?${parsedUrl.searchParams.toString()}`;
2018
- }
2045
+ //#region src/router/uri.ts
2019
2046
  /**
2020
- * Verify a signed URL using `crypto.subtle.verify()` (timing-attack-safe).
2047
+ * Encode a value for use as a path parameter.
2021
2048
  *
2022
- * @param url - Full URL or path with signature query param
2023
- * @param secret - HMAC secret key (same key used for signing)
2024
- * @returns true if signature is valid and not expired
2049
+ * Splits on `/` and encodes each segment with `encodeURIComponent`, so callers
2050
+ * can pass slash-containing values for catch-all params (e.g. `:slug{.+}`) and
2051
+ * still get a usable URL `'auth/login'` becomes `'auth/login'`, not
2052
+ * `'auth%2Flogin'`. Single segments behave exactly like `encodeURIComponent`.
2025
2053
  */
2026
- async function verifySignedUrl(url, secret) {
2027
- const parsedUrl = new URL(url, "https://placeholder.local");
2028
- const signature = parsedUrl.searchParams.get("signature");
2029
- if (!signature) return false;
2030
- const expires = parsedUrl.searchParams.get("expires");
2031
- if (expires) {
2032
- const expiryTime = parseInt(expires, 10);
2033
- if (isNaN(expiryTime) || Math.floor(Date.now() / 1e3) > expiryTime) return false;
2034
- }
2035
- parsedUrl.searchParams.delete("signature");
2036
- const dataToVerify = `${parsedUrl.pathname}?${parsedUrl.searchParams.toString()}`;
2037
- const base64 = signature.replace(/-/g, "+").replace(/_/g, "/");
2038
- const binaryStr = atob(base64);
2039
- const signatureBytes = new Uint8Array(binaryStr.length);
2040
- for (let i = 0; i < binaryStr.length; i++) signatureBytes[i] = binaryStr.charCodeAt(i);
2041
- const key = await importKey(secret);
2042
- const encoder = new TextEncoder();
2043
- return crypto.subtle.verify("HMAC", key, signatureBytes, encoder.encode(dataToVerify));
2054
+ function encodePathParam(value) {
2055
+ return value.split("/").map(encodeURIComponent).join("/");
2044
2056
  }
2045
- //#endregion
2046
- //#region src/router/uri.ts
2047
2057
  /**
2048
2058
  * Build a URL from a registered route, filling path/domain params and appending extras as query string.
2049
2059
  *
@@ -2067,7 +2077,7 @@ function buildRouteUrl(route, name, params) {
2067
2077
  for (const paramName of route.paramNames) {
2068
2078
  const value = allParams[paramName];
2069
2079
  if (value === void 0) throw new MissingRouteParamError(paramName, name, route.path);
2070
- url = url.replace(new RegExp(`:${paramName}(\\{[^}]*\\})?`), encodeURIComponent(value));
2080
+ url = url.replace(new RegExp(`:${paramName}(\\{[^}]*\\})?`), encodePathParam(value));
2071
2081
  consumedKeys.add(paramName);
2072
2082
  }
2073
2083
  let domain;
@@ -2090,9 +2100,11 @@ function buildRouteUrl(route, name, params) {
2090
2100
  }
2091
2101
  let Uri = class Uri {
2092
2102
  _defaults = {};
2093
- constructor(registry, routerContext) {
2103
+ trailingSlash;
2104
+ constructor(registry, routerContext, application) {
2094
2105
  this.registry = registry;
2095
2106
  this.routerContext = routerContext;
2107
+ this.trailingSlash = application.config.trailingSlash ?? "ignore";
2096
2108
  }
2097
2109
  /**
2098
2110
  * Set default URL parameters for this request.
@@ -2107,6 +2119,16 @@ let Uri = class Uri {
2107
2119
  };
2108
2120
  }
2109
2121
  /**
2122
+ * Read the currently configured default URL parameters.
2123
+ *
2124
+ * Used by frameworks that need to share these with the client (e.g. the
2125
+ * Inertia adapter ships them as a shared prop so `route()` calls in the
2126
+ * browser auto-fill the same sticky params as the server).
2127
+ */
2128
+ getDefaults() {
2129
+ return { ...this._defaults };
2130
+ }
2131
+ /**
2110
2132
  * Generate a URL from a named route.
2111
2133
  *
2112
2134
  * Keys matching `:param` placeholders fill the path.
@@ -2125,10 +2147,10 @@ let Uri = class Uri {
2125
2147
  route(name, params, options) {
2126
2148
  const registeredRoute = this.registry.get(name);
2127
2149
  if (!registeredRoute) throw new RouteNameNotFoundError(name);
2128
- let url = buildRouteUrl(registeredRoute, name, {
2150
+ let url = applyTrailingSlash(buildRouteUrl(registeredRoute, name, {
2129
2151
  ...this._defaults,
2130
2152
  ...params
2131
- });
2153
+ }), this.trailingSlash);
2132
2154
  if (options?.absolute && !url.startsWith("http")) url = `${new URL(this.routerContext.c.req.url).origin}${url}`;
2133
2155
  return url;
2134
2156
  }
@@ -2176,14 +2198,14 @@ let Uri = class Uri {
2176
2198
  * Get the current request URL pathname (without query string).
2177
2199
  */
2178
2200
  current() {
2179
- return new URL(this.routerContext.c.req.url).pathname;
2201
+ return applyTrailingSlash(new URL(this.routerContext.c.req.url).pathname, this.trailingSlash);
2180
2202
  }
2181
2203
  /**
2182
2204
  * Get the current request URL with query string (pathname + search).
2183
2205
  */
2184
2206
  full() {
2185
2207
  const parsed = new URL(this.routerContext.c.req.url);
2186
- return `${parsed.pathname}${parsed.search}`;
2208
+ return applyTrailingSlash(`${parsed.pathname}${parsed.search}`, this.trailingSlash);
2187
2209
  }
2188
2210
  /**
2189
2211
  * Get the previous request URL from the Referer header.
@@ -2215,7 +2237,7 @@ let Uri = class Uri {
2215
2237
  * @param options - URL generation options
2216
2238
  */
2217
2239
  to(path, queryParams, options) {
2218
- let url = path;
2240
+ let url = applyTrailingSlash(path, this.trailingSlash);
2219
2241
  if (queryParams) {
2220
2242
  const entries = Object.entries(queryParams);
2221
2243
  if (entries.length > 0) {
@@ -2235,7 +2257,7 @@ let Uri = class Uri {
2235
2257
  query(path, queryParams) {
2236
2258
  const parsed = new URL(path, "https://placeholder.local");
2237
2259
  for (const [key, value] of Object.entries(queryParams)) parsed.searchParams.set(key, value);
2238
- return `${parsed.pathname}${parsed.search}`;
2260
+ return applyTrailingSlash(`${parsed.pathname}${parsed.search}`, this.trailingSlash);
2239
2261
  }
2240
2262
  getAppSecret() {
2241
2263
  const secret = this.routerContext.c.env.APP_SECRET;
@@ -2247,7 +2269,12 @@ Uri = __decorate([
2247
2269
  Transient(),
2248
2270
  __decorateParam(0, inject(ROUTER_TOKENS.RouteRegistry)),
2249
2271
  __decorateParam(1, inject(ROUTER_TOKENS.RouterContext)),
2250
- __decorateMetadata("design:paramtypes", [Object, Object])
2272
+ __decorateParam(2, inject(DI_TOKENS.Application)),
2273
+ __decorateMetadata("design:paramtypes", [
2274
+ Object,
2275
+ Object,
2276
+ Object
2277
+ ])
2251
2278
  ], Uri);
2252
2279
  //#endregion
2253
2280
  //#region src/router/route-url.ts
@@ -2277,9 +2304,12 @@ Uri = __decorate([
2277
2304
  * ```
2278
2305
  */
2279
2306
  function route(name, params) {
2280
- const registeredRoute = getContainer().resolve(ROUTER_TOKENS.RouteRegistry).get(name);
2307
+ const container = getContainer();
2308
+ const registry = container.resolve(ROUTER_TOKENS.RouteRegistry);
2309
+ const application = container.resolve(DI_TOKENS.Application);
2310
+ const registeredRoute = registry.get(name);
2281
2311
  if (!registeredRoute) throw new RouteNameNotFoundError(name);
2282
- return buildRouteUrl(registeredRoute, name, params);
2312
+ return applyTrailingSlash(buildRouteUrl(registeredRoute, name, params), application.config.trailingSlash ?? "ignore");
2283
2313
  }
2284
2314
  //#endregion
2285
2315
  //#region src/router/middleware/verify-signature.middleware.ts
@@ -2457,6 +2487,6 @@ I18nModule = _I18nModule = __decorate([Module({ providers: [
2457
2487
  }
2458
2488
  ] })], I18nModule);
2459
2489
  //#endregion
2460
- export { Delete as A, MessageRegistry as B, successMessageSchema as C, getRouteDecoratedMethods as D, Route as E, createMiddlewareChain as F, buildDetectorOptions as G, getLocales as H, createDomainMiddleware as I, LocaleNotSupportedError as J, resolveI18nOptions as K, parseDomainPattern as L, Patch as M, Post as N, getRouteMetadata as O, Put as P, OpenAPIModule as R, paginationQuerySchema as S, validationErrorResponseSchema as T, getMessages as U, MessageLoaderService as V, messages as W, I18nContextMiddleware as X, OpenAPIConfigService as Y, sortRoutesBySpecificity as _, buildRouteUrl as a, errorResponseSchema as b, RouteRegistry as c, HonoApp as d, RouteRegistrationService as f, getPathSpecificityScore as g, generateConventionRouteName as h, Uri as i, Get as j, All as k, VersioningService as l, extractParamNames as m, VerifySignatureMiddleware as n, signUrl as o, extractDomainParamNames as p, TranslationMissingError as q, route as r, verifySignedUrl as s, I18nModule as t, LocalePathService as u, toOpenAPIPath as v, uuidParamSchema as w, paginatedResponseSchema as x, commonErrorSchemas as y, OpenAPIService as z };
2490
+ export { OpenAPIModule as A, LocaleNotSupportedError as B, validationErrorResponseSchema as C, createMiddlewareChain as D, getRouteMetadata as E, getMessages as F, I18nContextMiddleware as H, messages as I, buildDetectorOptions as L, MessageRegistry as M, MessageLoaderService as N, createDomainMiddleware as O, getLocales as P, resolveI18nOptions as R, uuidParamSchema as S, getRouteDecoratedMethods as T, OpenAPIConfigService as V, commonErrorSchemas as _, buildRouteUrl as a, paginationQuerySchema as b, LocalePathService as c, extractDomainParamNames as d, extractParamNames as f, toOpenAPIPath as g, sortRoutesBySpecificity as h, Uri as i, OpenAPIService as j, parseDomainPattern as k, HonoApp as l, getPathSpecificityScore as m, VerifySignatureMiddleware as n, RouteRegistry as o, generateConventionRouteName as p, route as r, VersioningService as s, I18nModule as t, RouteRegistrationService as u, errorResponseSchema as v, Route as w, successMessageSchema as x, paginatedResponseSchema as y, TranslationMissingError as z };
2461
2491
 
2462
- //# sourceMappingURL=i18n.module-BpLLLCTg.mjs.map
2492
+ //# sourceMappingURL=i18n.module-BBlNNlcG.mjs.map