stratal 0.0.26 → 0.1.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.
- package/CHANGELOG.md +912 -0
- package/README.md +96 -14
- package/dist/bin/cloudflare-workers-loader.mjs +18 -3
- package/dist/bin/cloudflare-workers-loader.mjs.map +1 -1
- package/dist/bin/quarry.mjs +85 -14
- package/dist/bin/quarry.mjs.map +1 -1
- package/dist/cache/index.d.mts +8 -9
- package/dist/cache/index.d.mts.map +1 -1
- package/dist/cache/index.mjs +56 -17
- package/dist/cache/index.mjs.map +1 -1
- package/dist/{cache.service-uElmBtdS.d.mts → cache.service-Dg0Hj0gp.d.mts} +27 -12
- package/dist/cache.service-Dg0Hj0gp.d.mts.map +1 -0
- package/dist/{command-DoBD2Cwl.d.mts → command-BXMccV_9.d.mts} +6 -3
- package/dist/command-BXMccV_9.d.mts.map +1 -0
- package/dist/{command-BvmUAPPQ.mjs → command-Bb820B6W.mjs} +36 -13
- package/dist/command-Bb820B6W.mjs.map +1 -0
- package/dist/common.schemas-BigQCwYB.mjs +89 -0
- package/dist/common.schemas-BigQCwYB.mjs.map +1 -0
- package/dist/config/index.d.mts +15 -15
- package/dist/config/index.d.mts.map +1 -1
- package/dist/config/index.mjs +4 -6
- package/dist/config/index.mjs.map +1 -1
- package/dist/{consumer-registry-D3iMTSdy.d.mts → consumer-registry-i9oCdarv.d.mts} +2 -3
- package/dist/consumer-registry-i9oCdarv.d.mts.map +1 -0
- package/dist/{container-storage-BmOJ4_Na.mjs → container-storage-DfF7SXdV.mjs} +11 -1
- package/dist/container-storage-DfF7SXdV.mjs.map +1 -0
- package/dist/{controller.decorator-YSTPQntu.mjs → controller.decorator-BB0JcwMg.mjs} +18 -9
- package/dist/controller.decorator-BB0JcwMg.mjs.map +1 -0
- package/dist/cron/index.d.mts +6 -7
- package/dist/cron/index.d.mts.map +1 -1
- package/dist/cron/index.mjs +1 -1
- package/dist/{cron-job-NesZRk8F.d.mts → cron-job-DAgNw4Bn.d.mts} +1 -2
- package/dist/cron-job-DAgNw4Bn.d.mts.map +1 -0
- package/dist/{cron.module-C81HTzR7.mjs → cron.module-Dnyl9sFk.mjs} +6 -7
- package/dist/{cron.module-C81HTzR7.mjs.map → cron.module-Dnyl9sFk.mjs.map} +1 -1
- package/dist/{decorate-CuAoSZvs.mjs → decorate-C79yt3Wt.mjs} +2 -2
- package/dist/di/index.d.mts +1 -1
- package/dist/di/index.mjs +2 -3
- package/dist/{di-D7qmrAir.mjs → di-DQ3xbjgf.mjs} +48 -6
- package/dist/di-DQ3xbjgf.mjs.map +1 -0
- package/dist/email/index.d.mts +158 -133
- package/dist/email/index.d.mts.map +1 -1
- package/dist/email/index.mjs +66 -78
- package/dist/email/index.mjs.map +1 -1
- package/dist/{en-CDZBMcc1.mjs → en-CCL84z4k.mjs} +2 -2
- package/dist/{en-CDZBMcc1.mjs.map → en-CCL84z4k.mjs.map} +1 -1
- package/dist/env-ug22bJj7.d.mts.map +1 -1
- package/dist/errors/index.d.mts +1 -1
- package/dist/errors/index.mjs +3 -3
- package/dist/{errors-C01O2T-n.mjs → errors-CrOXI_y5.mjs} +6 -8
- package/dist/errors-CrOXI_y5.mjs.map +1 -0
- package/dist/events/index.d.mts +8 -9
- package/dist/events/index.d.mts.map +1 -1
- package/dist/events/index.mjs +1 -1
- package/dist/{events-BhEQuT1X.mjs → events-GtwGEHyT.mjs} +5 -6
- package/dist/events-GtwGEHyT.mjs.map +1 -0
- package/dist/{exception-context-D-kvney-.mjs → exception-context-CdnGTQNF.mjs} +13 -7
- package/dist/exception-context-CdnGTQNF.mjs.map +1 -0
- package/dist/{gateway-context-m7kEzRa2.mjs → gateway-context-Cmc1NRS0.mjs} +7 -7
- package/dist/{gateway-context-m7kEzRa2.mjs.map → gateway-context-Cmc1NRS0.mjs.map} +1 -1
- package/dist/gateway-mode-0rUaeW3g.mjs +53 -0
- package/dist/gateway-mode-0rUaeW3g.mjs.map +1 -0
- package/dist/guards/index.d.mts +38 -20
- package/dist/guards/index.d.mts.map +1 -1
- package/dist/guards/index.mjs +2 -2
- package/dist/{guards-DALPXy3_.mjs → guards-DOGly9IF.mjs} +34 -11
- package/dist/guards-DOGly9IF.mjs.map +1 -0
- package/dist/hono-app-IhdqL0Fk.mjs +399 -0
- package/dist/hono-app-IhdqL0Fk.mjs.map +1 -0
- package/dist/{http-method.decorator-BljM8BDj.mjs → http-method.decorator-BDi9LTWj.mjs} +9 -8
- package/dist/http-method.decorator-BDi9LTWj.mjs.map +1 -0
- package/dist/i18n/index.d.mts +26 -137
- package/dist/i18n/index.d.mts.map +1 -1
- package/dist/i18n/index.mjs +4 -5
- package/dist/i18n/index.mjs.map +1 -1
- package/dist/i18n/messages/en/index.d.mts +1 -1
- package/dist/i18n/messages/en/index.mjs +1 -1
- package/dist/i18n/utils/index.d.mts +1 -2
- package/dist/i18n/utils/index.d.mts.map +1 -1
- package/dist/i18n/validation/index.d.mts +80 -5
- package/dist/i18n/validation/index.d.mts.map +1 -0
- package/dist/i18n/validation/index.mjs +4 -5
- package/dist/{i18n.module-B2DvWUPa.mjs → i18n.module-Bm3xlViA.mjs} +28 -13
- package/dist/i18n.module-Bm3xlViA.mjs.map +1 -0
- package/dist/index-0ItCjaqw.d.mts.map +1 -1
- package/dist/{index-CNuFQSNj.d.mts → index-3HJfhLlE.d.mts} +9 -11
- package/dist/index-3HJfhLlE.d.mts.map +1 -0
- package/dist/{index-uybm0bhQ.d.mts → index-B2WtEagR.d.mts} +1187 -234
- package/dist/index-B2WtEagR.d.mts.map +1 -0
- package/dist/index-BUt92sAE.d.mts.map +1 -1
- package/dist/index-CAHNCpvr.d.mts +203 -0
- package/dist/index-CAHNCpvr.d.mts.map +1 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +2 -2
- package/dist/{is-command-CEPO9n8c.mjs → is-command-CSVv14wh.mjs} +2 -2
- package/dist/{is-command-CEPO9n8c.mjs.map → is-command-CSVv14wh.mjs.map} +1 -1
- package/dist/{is-seeder-Gvh_AM71.mjs → is-seeder-gLgXYbqC.mjs} +2 -2
- package/dist/is-seeder-gLgXYbqC.mjs.map +1 -0
- package/dist/{lazy-module-loader-M6YKudNL.d.mts → lazy-module-loader-C3OHoUnR.d.mts} +4 -5
- package/dist/lazy-module-loader-C3OHoUnR.d.mts.map +1 -0
- package/dist/limit-BE0HNAF6.mjs +116 -0
- package/dist/limit-BE0HNAF6.mjs.map +1 -0
- package/dist/locale-path.service-BqzDIrgy.mjs +213 -0
- package/dist/locale-path.service-BqzDIrgy.mjs.map +1 -0
- package/dist/locale-url-nZrZxqJP.mjs.map +1 -1
- package/dist/{locale-url.service-6bgia24_.mjs → locale-url.service-H8PH6GGh.mjs} +15 -5
- package/dist/{locale-url.service-6bgia24_.mjs.map → locale-url.service-H8PH6GGh.mjs.map} +1 -1
- package/dist/logger/index.mjs +3 -5
- package/dist/logger/index.mjs.map +1 -1
- package/dist/macroable-cvDTFZ_A.mjs.map +1 -1
- package/dist/metadata-Ba8Fxs1F.mjs +48 -0
- package/dist/metadata-Ba8Fxs1F.mjs.map +1 -0
- package/dist/module/index.d.mts +8 -9
- package/dist/module/index.d.mts.map +1 -1
- package/dist/module/index.mjs +3 -4
- package/dist/module/index.mjs.map +1 -1
- package/dist/{module-registry-NxX5O0Qk.mjs → module-registry-lkQPDuQe.mjs} +17 -15
- package/dist/module-registry-lkQPDuQe.mjs.map +1 -0
- package/dist/{module.decorator-CYHY6pG5.mjs → module.decorator-CYbrOlX9.mjs} +2 -2
- package/dist/{module.decorator-CYHY6pG5.mjs.map → module.decorator-CYbrOlX9.mjs.map} +1 -1
- package/dist/openapi/index.d.mts +19 -16
- package/dist/openapi/index.d.mts.map +1 -1
- package/dist/openapi/index.mjs +2 -2
- package/dist/{openapi-CMwuCp31.mjs → openapi-DBfBa5Qh.mjs} +41 -65
- package/dist/openapi-DBfBa5Qh.mjs.map +1 -0
- package/dist/openapi-generator-CCwiu8AU.mjs +134 -0
- package/dist/openapi-generator-CCwiu8AU.mjs.map +1 -0
- package/dist/{openapi-tools.service-BC5EC3R3.mjs → openapi-tools.service-VKZJlTIX.mjs} +12 -4
- package/dist/openapi-tools.service-VKZJlTIX.mjs.map +1 -0
- package/dist/openapi.service-Ds-R1tih.d.mts +43 -0
- package/dist/openapi.service-Ds-R1tih.d.mts.map +1 -0
- package/dist/quarry/index.d.mts +10 -11
- package/dist/quarry/index.d.mts.map +1 -1
- package/dist/quarry/index.mjs +4 -4
- package/dist/quarry/runner.d.mts +19 -20
- package/dist/quarry/runner.d.mts.map +1 -1
- package/dist/quarry/runner.mjs +16 -13
- package/dist/quarry/runner.mjs.map +1 -1
- package/dist/{quarry-registry-DRnV-DDa.d.mts → quarry-registry-Ci4k58FZ.d.mts} +3 -4
- package/dist/quarry-registry-Ci4k58FZ.d.mts.map +1 -0
- package/dist/{quarry.module-CcGxU2dJ.mjs → quarry.module-BssRR8f0.mjs} +20 -37
- package/dist/quarry.module-BssRR8f0.mjs.map +1 -0
- package/dist/queue/index.d.mts +2 -2
- package/dist/queue/index.mjs +3 -3
- package/dist/queue/index.mjs.map +1 -1
- package/dist/{queue.module-CEs4_kEM.mjs → queue.module-DnAdSbR-.mjs} +40 -41
- package/dist/queue.module-DnAdSbR-.mjs.map +1 -0
- package/dist/{r2-storage.provider-BoZmR6Ut.mjs → r2-storage.provider-wU_6api4.mjs} +45 -6
- package/dist/r2-storage.provider-wU_6api4.mjs.map +1 -0
- package/dist/{rate-limit.decorator-Djs4oYDB.mjs → rate-limit.decorator-PYQ9EYqs.mjs} +10 -5
- package/dist/{rate-limit.decorator-Djs4oYDB.mjs.map → rate-limit.decorator-PYQ9EYqs.mjs.map} +1 -1
- package/dist/rate-limiter/index.d.mts +64 -19
- package/dist/rate-limiter/index.d.mts.map +1 -1
- package/dist/rate-limiter/index.mjs +67 -83
- package/dist/rate-limiter/index.mjs.map +1 -1
- package/dist/response-cache/index.d.mts +326 -0
- package/dist/response-cache/index.d.mts.map +1 -0
- package/dist/response-cache/index.mjs +511 -0
- package/dist/response-cache/index.mjs.map +1 -0
- package/dist/response-cache.error-B4D0zy9W.mjs +24 -0
- package/dist/response-cache.error-B4D0zy9W.mjs.map +1 -0
- package/dist/response-headers-BqA57S_X.mjs +158 -0
- package/dist/response-headers-BqA57S_X.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/route-cache-binding-CrLh8tB6.mjs +390 -0
- package/dist/route-cache-binding-CrLh8tB6.mjs.map +1 -0
- package/dist/route-metadata-QQhbed1i.mjs +17 -0
- package/dist/route-metadata-QQhbed1i.mjs.map +1 -0
- package/dist/{route-name-DGoBOfPg.mjs → route-name-BslSbmMP.mjs} +15 -11
- package/dist/route-name-BslSbmMP.mjs.map +1 -0
- package/dist/route-registration.service-COKBZTsn.mjs +1265 -0
- package/dist/route-registration.service-COKBZTsn.mjs.map +1 -0
- package/dist/{route-registry-BvLJisvK.mjs → route-registry-DFz9_-N6.mjs} +6 -6
- package/dist/{route-registry-BvLJisvK.mjs.map → route-registry-DFz9_-N6.mjs.map} +1 -1
- package/dist/router/index.d.mts +2 -2
- package/dist/router/index.mjs +16 -15
- package/dist/{router-resolver-sUV_jTrU.mjs → router-resolver-B-dMSMIG.mjs} +7 -3
- package/dist/router-resolver-B-dMSMIG.mjs.map +1 -0
- package/dist/{router-DwyqEXgf.mjs → router-wlsVkGkM.mjs} +5 -14
- package/dist/{router-DwyqEXgf.mjs.map → router-wlsVkGkM.mjs.map} +1 -1
- package/dist/seeder/index.d.mts +12 -13
- package/dist/seeder/index.d.mts.map +1 -1
- package/dist/seeder/index.mjs +4 -4
- package/dist/{seeder-BPGY5rUb.mjs → seeder-BrAk7XIj.mjs} +5 -6
- package/dist/{seeder-BPGY5rUb.mjs.map → seeder-BrAk7XIj.mjs.map} +1 -1
- package/dist/{seeder-registry-DEvCycsT.mjs → seeder-registry-Bo5BgLbC.mjs} +5 -6
- package/dist/{seeder-registry-DEvCycsT.mjs.map → seeder-registry-Bo5BgLbC.mjs.map} +1 -1
- package/dist/{seeder.module-CIwQbdN4.mjs → seeder.module-DOoVgTsK.mjs} +5 -5
- package/dist/{seeder.module-CIwQbdN4.mjs.map → seeder.module-DOoVgTsK.mjs.map} +1 -1
- package/dist/signed-url-DIU0sK_6.mjs.map +1 -1
- package/dist/storage/index.d.mts +63 -13
- package/dist/storage/index.d.mts.map +1 -1
- package/dist/storage/index.mjs +2 -2
- package/dist/storage/providers/index.d.mts +18 -16
- package/dist/storage/providers/index.d.mts.map +1 -1
- package/dist/storage/providers/index.mjs +1 -1
- package/dist/{storage-C30X81CS.mjs → storage-DBWVYMnn.mjs} +167 -49
- package/dist/storage-DBWVYMnn.mjs.map +1 -0
- package/dist/storage-provider.interface-CinHpjl1.d.mts +315 -0
- package/dist/storage-provider.interface-CinHpjl1.d.mts.map +1 -0
- package/dist/storage.error-Ao38Qdjx.mjs +7 -0
- package/dist/storage.error-Ao38Qdjx.mjs.map +1 -0
- package/dist/{stratal-D5j_I14G.d.mts → stratal-Bs1_E8GQ.d.mts} +13 -3
- package/dist/stratal-Bs1_E8GQ.d.mts.map +1 -0
- package/dist/{stratal-BL6FKUM_.mjs → stratal-CiwNpWy2.mjs} +56 -23
- package/dist/stratal-CiwNpWy2.mjs.map +1 -0
- package/dist/{tiered-cache.service-Dv3BhxxE.d.mts → tiered-cache.service-Bz8bbvyb.d.mts} +16 -3
- package/dist/tiered-cache.service-Bz8bbvyb.d.mts.map +1 -0
- package/dist/trailing-slash-2SctvePW.mjs.map +1 -1
- package/dist/types-CmV_9xBD.d.mts.map +1 -1
- package/dist/{uri-iwofWJ_T.mjs → uri-BrOq148R.mjs} +13 -9
- package/dist/{uri-iwofWJ_T.mjs.map → uri-BrOq148R.mjs.map} +1 -1
- package/dist/{usage-generator-DAWYasuP.mjs → usage-generator-BW7EcO-C.mjs} +3 -3
- package/dist/{usage-generator-DAWYasuP.mjs.map → usage-generator-BW7EcO-C.mjs.map} +1 -1
- package/dist/{zod-eKqqhZ5_.mjs → validation-CZ_B0aXY.mjs} +38 -17
- package/dist/validation-CZ_B0aXY.mjs.map +1 -0
- package/dist/{validation.context-CRvmrhq7.mjs → validation.context-CdbwQWIr.mjs} +2 -2
- package/dist/{validation.context-CRvmrhq7.mjs.map → validation.context-CdbwQWIr.mjs.map} +1 -1
- package/dist/{versioning.service-CCa2oYMJ.mjs → versioning.service-DnF_4Lnd.mjs} +5 -5
- package/dist/{versioning.service-CCa2oYMJ.mjs.map → versioning.service-DnF_4Lnd.mjs.map} +1 -1
- package/dist/websocket/index.d.mts +12 -13
- package/dist/websocket/index.d.mts.map +1 -1
- package/dist/websocket/index.mjs +1 -1
- package/dist/workers/index.d.mts +70 -7
- package/dist/workers/index.d.mts.map +1 -1
- package/dist/workers/index.mjs +72 -3
- package/dist/workers/index.mjs.map +1 -1
- package/dist/{zod-wecrEVAs.d.mts → zod-BGisqLs3.d.mts} +9 -16
- package/dist/zod-BGisqLs3.d.mts.map +1 -0
- package/package.json +33 -25
- package/dist/cache.service-uElmBtdS.d.mts.map +0 -1
- package/dist/chunk-BBjsoOtd.mjs +0 -27
- package/dist/command-BvmUAPPQ.mjs.map +0 -1
- package/dist/command-DoBD2Cwl.d.mts.map +0 -1
- package/dist/consumer-registry-D3iMTSdy.d.mts.map +0 -1
- package/dist/container-storage-BmOJ4_Na.mjs.map +0 -1
- package/dist/controller.decorator-YSTPQntu.mjs.map +0 -1
- package/dist/cron-job-NesZRk8F.d.mts.map +0 -1
- package/dist/di-D7qmrAir.mjs.map +0 -1
- package/dist/errors-C01O2T-n.mjs.map +0 -1
- package/dist/events-BhEQuT1X.mjs.map +0 -1
- package/dist/exception-context-D-kvney-.mjs.map +0 -1
- package/dist/guards-DALPXy3_.mjs.map +0 -1
- package/dist/hono-app-COAgmutc.mjs +0 -168
- package/dist/hono-app-COAgmutc.mjs.map +0 -1
- package/dist/http-method.decorator-BljM8BDj.mjs.map +0 -1
- package/dist/i18n.module-B2DvWUPa.mjs.map +0 -1
- package/dist/index-CNuFQSNj.d.mts.map +0 -1
- package/dist/index-DtBNIFuP.d.mts +0 -42
- package/dist/index-DtBNIFuP.d.mts.map +0 -1
- package/dist/index-HgOLNruQ.d.mts +0 -203
- package/dist/index-HgOLNruQ.d.mts.map +0 -1
- package/dist/index-uybm0bhQ.d.mts.map +0 -1
- package/dist/is-seeder-Gvh_AM71.mjs.map +0 -1
- package/dist/lazy-module-loader-M6YKudNL.d.mts.map +0 -1
- package/dist/locale-path.service-CH0CaxwH.mjs +0 -165
- package/dist/locale-path.service-CH0CaxwH.mjs.map +0 -1
- package/dist/metadata-DzzprcID.mjs +0 -39
- package/dist/metadata-DzzprcID.mjs.map +0 -1
- package/dist/module-registry-NxX5O0Qk.mjs.map +0 -1
- package/dist/openapi-CMwuCp31.mjs.map +0 -1
- package/dist/openapi-tools.service-BC5EC3R3.mjs.map +0 -1
- package/dist/openapi.service-2rvJBCEg.d.mts +0 -50
- package/dist/openapi.service-2rvJBCEg.d.mts.map +0 -1
- package/dist/quarry-registry-DRnV-DDa.d.mts.map +0 -1
- package/dist/quarry.module-CcGxU2dJ.mjs.map +0 -1
- package/dist/queue.module-CEs4_kEM.mjs.map +0 -1
- package/dist/r2-storage.provider-BoZmR6Ut.mjs.map +0 -1
- package/dist/route-name-DGoBOfPg.mjs.map +0 -1
- package/dist/route-registration.service-CDPQKpm4.mjs +0 -918
- package/dist/route-registration.service-CDPQKpm4.mjs.map +0 -1
- package/dist/router-resolver-sUV_jTrU.mjs.map +0 -1
- package/dist/storage-C30X81CS.mjs.map +0 -1
- package/dist/storage-provider.interface-ClUwxz4S.d.mts +0 -201
- package/dist/storage-provider.interface-ClUwxz4S.d.mts.map +0 -1
- package/dist/storage.error-BStXPmO4.mjs +0 -8
- package/dist/storage.error-BStXPmO4.mjs.map +0 -1
- package/dist/stratal-BL6FKUM_.mjs.map +0 -1
- package/dist/stratal-D5j_I14G.d.mts.map +0 -1
- package/dist/tiered-cache.service-Dv3BhxxE.d.mts.map +0 -1
- package/dist/validation-CpOjviyT.mjs +0 -49
- package/dist/validation-CpOjviyT.mjs.map +0 -1
- package/dist/zod-eKqqhZ5_.mjs.map +0 -1
- package/dist/zod-wecrEVAs.d.mts.map +0 -1
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { C as ROUTER_TOKENS, _ as defineMetadata, g as getMethodInjections, l as Singleton, p as inject, r as DI_TOKENS, v as getMetadata } from "./di-DQ3xbjgf.mjs";
|
|
3
|
+
import { n as __decorateParam, t as __decorate } from "./decorate-C79yt3Wt.mjs";
|
|
4
|
+
import { LOGGER_TOKENS } from "./logger/index.mjs";
|
|
5
|
+
import { d as HttpException, f as abort } from "./errors-CrOXI_y5.mjs";
|
|
6
|
+
import { a as RouterContext, c as METHOD_STATUS_CODES, d as SECURITY_SCHEMES, l as ROUTER_CONTEXT_KEYS, o as DEFAULT_CONTENT_TYPE, s as HTTP_METHODS, u as ROUTE_METADATA_KEYS } from "./exception-context-CdnGTQNF.mjs";
|
|
7
|
+
import { o as RouterError, s as createThrottleMiddleware } from "./module-registry-lkQPDuQe.mjs";
|
|
8
|
+
import { t as isGatewayMode } from "./gateway-mode-0rUaeW3g.mjs";
|
|
9
|
+
import { i as getControllerRoute, r as getControllerOptions } from "./controller.decorator-BB0JcwMg.mjs";
|
|
10
|
+
import { i as getMethodGuards, r as getControllerGuards, t as GuardExecutionService } from "./guards-DOGly9IF.mjs";
|
|
11
|
+
import { r as ResponseCacheConfigError, t as CachePurgeError } from "./response-cache.error-B4D0zy9W.mjs";
|
|
12
|
+
import { c as getCacheable, i as cachingUnavailableReason, l as RESPONSE_CACHE_TOKENS, o as getPurgesCache, t as bindRouteCache, u as createLoopbackPurgeTarget } from "./route-cache-binding-CrLh8tB6.mjs";
|
|
13
|
+
import { n as getRateLimits } from "./rate-limit.decorator-PYQ9EYqs.mjs";
|
|
14
|
+
import { a as getWsOnCloseMethod, o as getWsOnErrorMethod, s as getWsOnMessageMethod, t as GatewayContext, u as isGateway } from "./gateway-context-Cmc1NRS0.mjs";
|
|
15
|
+
import { i as extractPathParams, r as generateConventionRouteName, s as toOpenAPIPath } from "./route-name-BslSbmMP.mjs";
|
|
16
|
+
import { object } from "zod/mini";
|
|
17
|
+
import { validator } from "hono/validator";
|
|
18
|
+
//#region src/router/errors/schema-validation.error.ts
|
|
19
|
+
var SchemaValidationError = class extends HttpException {
|
|
20
|
+
issues;
|
|
21
|
+
constructor(zodError) {
|
|
22
|
+
super(400, "Schema validation failed");
|
|
23
|
+
this.issues = zodError.issues.map((err) => ({
|
|
24
|
+
path: err.path.join("."),
|
|
25
|
+
message: err.message,
|
|
26
|
+
code: err.code
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
reportContext() {
|
|
30
|
+
return { issues: this.issues };
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/router/errors/index.ts
|
|
35
|
+
/**
|
|
36
|
+
* Error thrown when a signed URL has an invalid or expired signature.
|
|
37
|
+
*
|
|
38
|
+
* HTTP Status: 403 Forbidden
|
|
39
|
+
*/
|
|
40
|
+
var InvalidSignatureError = class extends HttpException {
|
|
41
|
+
constructor() {
|
|
42
|
+
super(403, "Invalid or expired signature");
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* ResponseValidationError
|
|
47
|
+
*
|
|
48
|
+
* Thrown when a controller's response body does not match the declared Zod response schema.
|
|
49
|
+
* Indicates a server-side schema mismatch — the controller is returning data that
|
|
50
|
+
* violates its own API contract.
|
|
51
|
+
*/
|
|
52
|
+
var ResponseValidationError = class extends HttpException {
|
|
53
|
+
issues;
|
|
54
|
+
constructor(zodError) {
|
|
55
|
+
super(500, "Response validation failed");
|
|
56
|
+
this.issues = zodError.issues.map((err) => ({
|
|
57
|
+
path: err.path.join("."),
|
|
58
|
+
message: err.message,
|
|
59
|
+
code: err.code
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/router/middleware/domain.middleware.ts
|
|
65
|
+
/**
|
|
66
|
+
* Parse a domain pattern into a regex and extract parameter names.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* parseDomainPattern('{tenant}.example.com')
|
|
70
|
+
* // => { regex: /^([^.]+)\.example\.com$/, paramNames: ['tenant'] }
|
|
71
|
+
*
|
|
72
|
+
* parseDomainPattern('{region}.{tenant}.example.com')
|
|
73
|
+
* // => { regex: /^([^.]+)\.([^.]+)\.example\.com$/, paramNames: ['region', 'tenant'] }
|
|
74
|
+
*/
|
|
75
|
+
function parseDomainPattern(pattern) {
|
|
76
|
+
const paramNames = [];
|
|
77
|
+
const escaped = pattern.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_match, paramName) => {
|
|
78
|
+
paramNames.push(paramName);
|
|
79
|
+
return "([^.]+)";
|
|
80
|
+
}).replace(/\./g, "\\.");
|
|
81
|
+
return {
|
|
82
|
+
regex: new RegExp(`^${escaped}$`),
|
|
83
|
+
paramNames
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Strip port number from a host header value.
|
|
88
|
+
* 'example.com:8787' => 'example.com'
|
|
89
|
+
*/
|
|
90
|
+
function stripPort(host) {
|
|
91
|
+
const colonIdx = host.lastIndexOf(":");
|
|
92
|
+
if (colonIdx === -1) return host;
|
|
93
|
+
const afterColon = host.slice(colonIdx + 1);
|
|
94
|
+
return /^\d+$/.test(afterColon) ? host.slice(0, colonIdx) : host;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create a Hono middleware that matches the request host against a domain pattern.
|
|
98
|
+
*
|
|
99
|
+
* When the host matches, domain parameters are extracted and stored in context
|
|
100
|
+
* variables accessible via `ctx.domain(key)`.
|
|
101
|
+
*
|
|
102
|
+
* When the host does NOT match, aborts with 404.
|
|
103
|
+
*
|
|
104
|
+
* @param pattern - Domain pattern with `{param}` placeholders (e.g., '{tenant}.myapp.com')
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* // Applied automatically by RouteRegistrationService for controllers with domain config
|
|
109
|
+
* @Controller('/dashboard', { domain: '{tenant}.myapp.com' })
|
|
110
|
+
* export class DashboardController {
|
|
111
|
+
* async index(ctx: RouterContext) {
|
|
112
|
+
* const tenant = ctx.domain('tenant')
|
|
113
|
+
* }
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
function createDomainMiddleware(pattern) {
|
|
118
|
+
const { regex, paramNames } = parseDomainPattern(pattern);
|
|
119
|
+
return async (c, next) => {
|
|
120
|
+
const host = stripPort(c.req.header("host") ?? "");
|
|
121
|
+
const match = regex.exec(host);
|
|
122
|
+
if (!match) abort(404, "Domain mismatch");
|
|
123
|
+
for (let i = 0; i < paramNames.length; i++) c.set(`domain:${paramNames[i]}`, match[i + 1]);
|
|
124
|
+
await next();
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/router/middleware/middleware-chain.ts
|
|
129
|
+
/**
|
|
130
|
+
* Create a Hono middleware handler that executes a chain of Stratal middleware classes.
|
|
131
|
+
*
|
|
132
|
+
* Each middleware is resolved from the request-scoped container per request,
|
|
133
|
+
* then executed in order (first registered = outermost in the chain).
|
|
134
|
+
*
|
|
135
|
+
* @param classes - Middleware classes to chain
|
|
136
|
+
* @returns Hono middleware handler
|
|
137
|
+
*/
|
|
138
|
+
function createMiddlewareChain(classes) {
|
|
139
|
+
return async (c, next) => {
|
|
140
|
+
const requestContainer = c.get(ROUTER_CONTEXT_KEYS.REQUEST_CONTAINER);
|
|
141
|
+
const ctx = new RouterContext(c);
|
|
142
|
+
let current = next;
|
|
143
|
+
for (let i = classes.length - 1; i >= 0; i--) {
|
|
144
|
+
const prevNext = current;
|
|
145
|
+
const middlewareClass = classes[i];
|
|
146
|
+
current = async () => {
|
|
147
|
+
const middleware = requestContainer.resolve(middlewareClass);
|
|
148
|
+
let called = false;
|
|
149
|
+
const guardedNext = () => {
|
|
150
|
+
if (called) return Promise.reject(new RouterError(`Middleware "${middlewareClass.name ?? "anonymous"}" called next() multiple times`));
|
|
151
|
+
called = true;
|
|
152
|
+
return prevNext();
|
|
153
|
+
};
|
|
154
|
+
const result = await middleware.handle(ctx, guardedNext);
|
|
155
|
+
if (result instanceof Response) c.res = result;
|
|
156
|
+
return result;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const result = await current();
|
|
160
|
+
if (result instanceof Response) return result;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/router/decorators/route.decorator.ts
|
|
165
|
+
/**
|
|
166
|
+
* Decorator to add OpenAPI metadata to a controller method using convention-based routing.
|
|
167
|
+
*
|
|
168
|
+
* **Cannot be mixed with HTTP method decorators** (`@Get`, `@Post`, `@Put`, `@Patch`,
|
|
169
|
+
* `@Delete`, `@All`) in the same controller. Use one pattern or the other.
|
|
170
|
+
*
|
|
171
|
+
* Stores route configuration (schemas, response, tags, security) in metadata.
|
|
172
|
+
* HTTP method, path, and success status code are auto-derived from the method name:
|
|
173
|
+
* - index() → GET /base-path → 200
|
|
174
|
+
* - show() → GET /base-path/:id → 200
|
|
175
|
+
* - create() → POST /base-path → 201
|
|
176
|
+
* - update() → PUT /base-path/:id → 200
|
|
177
|
+
* - patch() → PATCH /base-path/:id → 200
|
|
178
|
+
* - destroy() → DELETE /base-path/:id → 200
|
|
179
|
+
*
|
|
180
|
+
* @param config - Route configuration (schemas, response, tags, security)
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* @Controller('/api/v1/notes', {
|
|
185
|
+
* tags: ['Notes'],
|
|
186
|
+
* security: ['bearerAuth']
|
|
187
|
+
* })
|
|
188
|
+
* export class NotesController implements Controller {
|
|
189
|
+
* @Route({
|
|
190
|
+
* body: createNoteSchema,
|
|
191
|
+
* response: noteSchema, // 201 auto-derived from 'create' method
|
|
192
|
+
* tags: ['Mutations'],
|
|
193
|
+
* description: 'Create a new note'
|
|
194
|
+
* })
|
|
195
|
+
* async create(ctx: RouterContext): Promise<Response> {
|
|
196
|
+
* // POST /api/v1/notes (auto-derived from method name)
|
|
197
|
+
* // Body schema: createNoteSchema (auto-validated)
|
|
198
|
+
* // Response: 201 → noteSchema (status auto-derived)
|
|
199
|
+
* // Tags: ['Notes', 'Mutations'] (merged with controller)
|
|
200
|
+
* // Security: ['bearerAuth'] (inherited from controller)
|
|
201
|
+
* const body = ctx.body()
|
|
202
|
+
* const note = await this.notesService.create(body)
|
|
203
|
+
* return ctx.json(note, 201)
|
|
204
|
+
* }
|
|
205
|
+
*
|
|
206
|
+
* @Route({
|
|
207
|
+
* query: paginationSchema,
|
|
208
|
+
* response: array(noteSchema) // 200 auto-derived from 'index' method
|
|
209
|
+
* })
|
|
210
|
+
* async index(ctx: RouterContext): Promise<Response> {
|
|
211
|
+
* // GET /api/v1/notes (auto-derived)
|
|
212
|
+
* // Query params auto-validated
|
|
213
|
+
* const notes = await this.notesService.list()
|
|
214
|
+
* return ctx.json(notes)
|
|
215
|
+
* }
|
|
216
|
+
*
|
|
217
|
+
* @Route({
|
|
218
|
+
* params: object({ id: uuid() }),
|
|
219
|
+
* response: {
|
|
220
|
+
* schema: noteSchema,
|
|
221
|
+
* description: 'Note details'
|
|
222
|
+
* },
|
|
223
|
+
* security: [] // Override to make public
|
|
224
|
+
* })
|
|
225
|
+
* async show(ctx: RouterContext): Promise<Response> {
|
|
226
|
+
* // GET /api/v1/notes/:id (auto-derived)
|
|
227
|
+
* // URL params auto-validated
|
|
228
|
+
* // Response: 200 → noteSchema (status auto-derived)
|
|
229
|
+
* // Security: [] (public route, override controller security)
|
|
230
|
+
* const id = ctx.param('id')
|
|
231
|
+
* const note = await this.notesService.findById(id)
|
|
232
|
+
* return ctx.json(note)
|
|
233
|
+
* }
|
|
234
|
+
* }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
function Route(config) {
|
|
238
|
+
return function(target, propertyKey, descriptor) {
|
|
239
|
+
const metadata = {
|
|
240
|
+
type: "convention",
|
|
241
|
+
config
|
|
242
|
+
};
|
|
243
|
+
defineMetadata(ROUTE_METADATA_KEYS.ROUTE_CONFIG, metadata, target, propertyKey);
|
|
244
|
+
const existing = getMetadata(ROUTE_METADATA_KEYS.DECORATED_METHODS, target) ?? [];
|
|
245
|
+
existing.push(propertyKey);
|
|
246
|
+
defineMetadata(ROUTE_METADATA_KEYS.DECORATED_METHODS, existing, target);
|
|
247
|
+
return descriptor;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get the route metadata from a controller method
|
|
252
|
+
*
|
|
253
|
+
* @param target - Controller instance or prototype
|
|
254
|
+
* @param methodName - Name of the method
|
|
255
|
+
* @returns Route metadata or undefined if not decorated
|
|
256
|
+
*/
|
|
257
|
+
function getRouteMetadata(target, methodName) {
|
|
258
|
+
return getMetadata(ROUTE_METADATA_KEYS.ROUTE_CONFIG, target, methodName);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get all methods with route decorators (@Route, @Get, @Post, etc.) from a controller
|
|
262
|
+
*
|
|
263
|
+
* @param ControllerClass - Controller class
|
|
264
|
+
* @returns Array of method names that have route metadata
|
|
265
|
+
*/
|
|
266
|
+
function getRouteDecoratedMethods(ControllerClass) {
|
|
267
|
+
const methods = /* @__PURE__ */ new Set();
|
|
268
|
+
let proto = ControllerClass.prototype;
|
|
269
|
+
while (proto && proto !== Object.prototype) {
|
|
270
|
+
const own = getMetadata(ROUTE_METADATA_KEYS.DECORATED_METHODS, proto);
|
|
271
|
+
if (own) for (const m of own) methods.add(m);
|
|
272
|
+
proto = Object.getPrototypeOf(proto);
|
|
273
|
+
}
|
|
274
|
+
return [...methods];
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/router/services/capture-payload.ts
|
|
278
|
+
/** Does any declared tag template read the `{data.*}` scope? */
|
|
279
|
+
function readsDataScope(tags) {
|
|
280
|
+
return (tags ?? []).some((tag) => tag.includes("{data."));
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Whether this route needs its handler's JSON payload captured.
|
|
284
|
+
*
|
|
285
|
+
* Evaluated once at route registration, never per request: capturing
|
|
286
|
+
* unconditionally would clone and re-parse every JSON response body in the
|
|
287
|
+
* application, doubling parse cost on a hot path that has nothing to do with
|
|
288
|
+
* caching.
|
|
289
|
+
*/
|
|
290
|
+
function needsPayloadCapture(cacheable, purges) {
|
|
291
|
+
return readsDataScope(cacheable?.tags) || readsDataScope(purges?.tags);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Matches the JSON family: `application/json` plus any `+json` structured
|
|
295
|
+
* suffix (`application/problem+json`, `application/ld+json`,
|
|
296
|
+
* `application/vnd.api+json`), with or without parameters.
|
|
297
|
+
*/
|
|
298
|
+
const JSON_CONTENT_TYPE = /^application\/(?:[\w.-]+\+)?json\s*(?:;|$)/i;
|
|
299
|
+
/**
|
|
300
|
+
* Record the handler's structured payload so `{data.*}` cache tags can read
|
|
301
|
+
* values the request itself never carried — a post's category ID on a publish
|
|
302
|
+
* route.
|
|
303
|
+
*
|
|
304
|
+
* **This does not decide what is cacheable.** Every content type Workers
|
|
305
|
+
* Caching stores — HTML, text, images, binary, streams — is cached exactly the
|
|
306
|
+
* same way; `@Cacheable` is content-type agnostic. This function only decides
|
|
307
|
+
* whether a tag can interpolate a value *out of the response body*, which
|
|
308
|
+
* requires a body that parses into an object. Non-JSON routes still cache
|
|
309
|
+
* normally and can still tag from `{param.*}` and `{query.*}` — never
|
|
310
|
+
* `{body.*}`, which `bindRouteCache` rejects at registration because the
|
|
311
|
+
* router never has a parsed request body available to populate it.
|
|
312
|
+
* Inertia sets the payload directly when building its page response, so its
|
|
313
|
+
* HTML documents support
|
|
314
|
+
* `{data.*}` without any parsing here.
|
|
315
|
+
*
|
|
316
|
+
* Clones before reading: consuming the returned Response's body would leave
|
|
317
|
+
* nothing to send.
|
|
318
|
+
*/
|
|
319
|
+
async function capturePayload(c, response) {
|
|
320
|
+
if (c.get(ROUTER_CONTEXT_KEYS.RESPONSE_PAYLOAD)) return;
|
|
321
|
+
if (!JSON_CONTENT_TYPE.test(response.headers.get("Content-Type") ?? "")) return;
|
|
322
|
+
try {
|
|
323
|
+
c.set(ROUTER_CONTEXT_KEYS.RESPONSE_PAYLOAD, await response.clone().json());
|
|
324
|
+
} catch {}
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/router/validation/create-validator.ts
|
|
328
|
+
/**
|
|
329
|
+
* Build a Hono validator middleware that runs a Zod schema against one request
|
|
330
|
+
* part and populates `c.req.valid(target)` with the parsed (coerced) output.
|
|
331
|
+
*
|
|
332
|
+
* This is the plain-Hono replacement for `@hono/zod-openapi`'s `app.openapi()`
|
|
333
|
+
* validator composition. It carries **no runtime `zod` import** — it only calls
|
|
334
|
+
* `safeParseAsync` on the caller-supplied schema instance and references
|
|
335
|
+
* `ZodType`/`ZodError` as erased types — so a route without a declared schema
|
|
336
|
+
* attaches no validator and pulls in no `zod`.
|
|
337
|
+
*
|
|
338
|
+
* Parsing is async so a schema may carry an async refinement — one that reaches
|
|
339
|
+
* a database, a cache or a service binding to decide whether a value is
|
|
340
|
+
* acceptable. Zod's synchronous `safeParse` throws `$ZodAsyncError` rather than
|
|
341
|
+
* returning a failure when it meets one, which would surface as a 500 instead
|
|
342
|
+
* of a 400. A fully synchronous schema resolves on the same tick.
|
|
343
|
+
*
|
|
344
|
+
* On failure it throws {@link SchemaValidationError}, which the global exception
|
|
345
|
+
* handler renders as a 400 with i18n-translated issues (the Zod error map is
|
|
346
|
+
* installed globally by the I18n module, so messages stay localized).
|
|
347
|
+
*
|
|
348
|
+
* `omit` drops framework-injected request keys (e.g. the `locale` path segment
|
|
349
|
+
* on localized routes) before parsing, so they never reach a user's schema —
|
|
350
|
+
* keeping a `.strict()` schema from rejecting them and keeping `c.req.valid()`
|
|
351
|
+
* limited to the keys the schema actually declares.
|
|
352
|
+
*/
|
|
353
|
+
function createValidator(target, schema, omit) {
|
|
354
|
+
return validator(target, async (value) => {
|
|
355
|
+
const record = value;
|
|
356
|
+
const input = omit?.length ? omitKeys(record, omit) : record;
|
|
357
|
+
const result = await schema.safeParseAsync(input);
|
|
358
|
+
if (!result.success) throw new SchemaValidationError(result.error);
|
|
359
|
+
return result.data;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function omitKeys(value, keys) {
|
|
363
|
+
const out = {};
|
|
364
|
+
for (const key in value) if (!keys.includes(key)) out[key] = value[key];
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/router/validation/route-validators.ts
|
|
369
|
+
/** The framework-injected path key on localized routes; never part of a user schema. */
|
|
370
|
+
const LOCALE_PARAM_KEY = "locale";
|
|
371
|
+
/** Map a body content type to the Hono validation target. */
|
|
372
|
+
function bodyTarget(contentType) {
|
|
373
|
+
return contentType.includes("json") ? "json" : "form";
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Assemble the ordered validator middlewares for a route from its declared
|
|
377
|
+
* schemas. Order matches Hono's expectation (param → query → body) and only
|
|
378
|
+
* includes validators for parts the route actually declares.
|
|
379
|
+
*/
|
|
380
|
+
function buildRouteValidators(schemas) {
|
|
381
|
+
const validators = [];
|
|
382
|
+
if (schemas.params) validators.push(createValidator("param", schemas.params, schemas.isLocaleVariant ? [LOCALE_PARAM_KEY] : void 0));
|
|
383
|
+
if (schemas.query) validators.push(createValidator("query", schemas.query));
|
|
384
|
+
if (schemas.body) validators.push(createValidator(bodyTarget(schemas.body.contentType), schemas.body.schema));
|
|
385
|
+
return validators;
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/router/services/route-registration.service.ts
|
|
389
|
+
var route_registration_service_exports = /* @__PURE__ */ __exportAll({ RouteRegistrationService: () => RouteRegistrationService });
|
|
390
|
+
const invokeHandler = async (instance, method, ...args) => {
|
|
391
|
+
return await instance[method](...args);
|
|
392
|
+
};
|
|
393
|
+
/** True for anything with a callable `.then` — a `Promise`, or a thenable. */
|
|
394
|
+
function isThenable(value) {
|
|
395
|
+
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
396
|
+
}
|
|
397
|
+
let RouteRegistrationService = class RouteRegistrationService {
|
|
398
|
+
logger;
|
|
399
|
+
registry;
|
|
400
|
+
routerResolver;
|
|
401
|
+
localePathService;
|
|
402
|
+
app;
|
|
403
|
+
moduleRegistry;
|
|
404
|
+
metadataRegistry;
|
|
405
|
+
cacheability;
|
|
406
|
+
responseCache;
|
|
407
|
+
responseCacheOptions;
|
|
408
|
+
gatewayRouteTable;
|
|
409
|
+
partitionResolver;
|
|
410
|
+
controllerClasses = /* @__PURE__ */ new Map();
|
|
411
|
+
upgradeWebSocketFn = null;
|
|
412
|
+
cacheableRouteCount = 0;
|
|
413
|
+
/**
|
|
414
|
+
* Whether Workers Caching is usable, answered once per entrypoint.
|
|
415
|
+
*
|
|
416
|
+
* Keyed on whether the request arrived through the gateway, because that is
|
|
417
|
+
* the scope the answer actually has: a gateway topology runs two entrypoints
|
|
418
|
+
* — `cache.enabled` false on the gateway, true on the cached one — over a
|
|
419
|
+
* single isolate and therefore a single instance of this service. A verdict
|
|
420
|
+
* reached on one says nothing about the other, so holding one for the
|
|
421
|
+
* isolate lets whichever entrypoint happens to serve the first request
|
|
422
|
+
* decide for both, and a `cache.enabled` that is false *by design* on the
|
|
423
|
+
* gateway reads as the app being misconfigured.
|
|
424
|
+
*
|
|
425
|
+
* Memoized rather than recomputed because `cache.enabled` is a static
|
|
426
|
+
* Wrangler setting: within one entrypoint the answer cannot change.
|
|
427
|
+
*/
|
|
428
|
+
cachingAvailability = /* @__PURE__ */ new Map();
|
|
429
|
+
constructor(logger, registry, routerResolver, localePathService, app, moduleRegistry, metadataRegistry, cacheability, responseCache, responseCacheOptions, gatewayRouteTable, partitionResolver) {
|
|
430
|
+
this.logger = logger;
|
|
431
|
+
this.registry = registry;
|
|
432
|
+
this.routerResolver = routerResolver;
|
|
433
|
+
this.localePathService = localePathService;
|
|
434
|
+
this.app = app;
|
|
435
|
+
this.moduleRegistry = moduleRegistry;
|
|
436
|
+
this.metadataRegistry = metadataRegistry;
|
|
437
|
+
this.cacheability = cacheability;
|
|
438
|
+
this.responseCache = responseCache;
|
|
439
|
+
this.responseCacheOptions = responseCacheOptions;
|
|
440
|
+
this.gatewayRouteTable = gatewayRouteTable;
|
|
441
|
+
this.partitionResolver = partitionResolver;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* The cached entrypoint named by `forRoot({ gateway: { entrypoint } })`, or
|
|
445
|
+
* `undefined` when no gateway is configured.
|
|
446
|
+
*
|
|
447
|
+
* A `Promise` (the `forRootAsync` trap) reads as "no gateway" here rather
|
|
448
|
+
* than throwing: `responseCacheDefaults()` raises that as its own boot error
|
|
449
|
+
* with a message about the real problem, and it is called first on every
|
|
450
|
+
* path that consults this.
|
|
451
|
+
*/
|
|
452
|
+
gatewayEntrypoint() {
|
|
453
|
+
const options = this.responseCacheOptions;
|
|
454
|
+
if (isThenable(options)) return void 0;
|
|
455
|
+
return options?.gateway?.entrypoint;
|
|
456
|
+
}
|
|
457
|
+
/** `gateway.keyBy`, read under the same `forRootAsync` caveat as the entrypoint. */
|
|
458
|
+
gatewayKeyBy() {
|
|
459
|
+
const options = this.responseCacheOptions;
|
|
460
|
+
if (isThenable(options)) return [];
|
|
461
|
+
return options?.gateway?.keyBy ?? [];
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* `ResponseCacheModule.forRoot({ defaults })`, resolved once at
|
|
465
|
+
* construction (constructor injection is memoized on this singleton
|
|
466
|
+
* instance) and falling back to `{}` when the module was never imported —
|
|
467
|
+
* mirrors `OpenAPIConfigStore.getBaseConfig()`.
|
|
468
|
+
*
|
|
469
|
+
* `forRootAsync({ useFactory })` is a trap here: `ModuleRegistry` registers
|
|
470
|
+
* the factory verbatim (`registerFactory`), and `Container.resolve()` never
|
|
471
|
+
* awaits a factory's return value — so an async `useFactory` resolves this
|
|
472
|
+
* token to a **Promise**, not the options object. Left unchecked, `?.defaults`
|
|
473
|
+
* on a Promise is `undefined`, silently falling back to `{}` — including
|
|
474
|
+
* silently dropping a `partitionBy` the author thought was enforced, the
|
|
475
|
+
* exact per-user data leak this feature exists to prevent. Detected and
|
|
476
|
+
* thrown here, once, at boot, rather than trusted per route.
|
|
477
|
+
*/
|
|
478
|
+
responseCacheDefaults() {
|
|
479
|
+
const options = this.responseCacheOptions;
|
|
480
|
+
if (isThenable(options)) throw new ResponseCacheConfigError("ResponseCacheModule.forRootAsync() options resolved to a Promise, not the awaited value. Route registration reads `defaults` synchronously — an async `useFactory` is never awaited before routes register, so `defaults` (and any `partitionBy` inside it) would be silently dropped instead of enforced, and every `@Cacheable` route would register as if no defaults were set. Use a synchronous `useFactory`, or resolve the options yourself and register them with `useValue` once the async work has settled.");
|
|
481
|
+
return options?.defaults ?? {};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Configure router with controllers and global middleware.
|
|
485
|
+
* Resolves controllers from ModuleRegistry and global middleware from RouterResolver.
|
|
486
|
+
*/
|
|
487
|
+
async configure() {
|
|
488
|
+
const controllers = this.moduleRegistry.getAllControllers();
|
|
489
|
+
const globalMiddleware = this.routerResolver?.getGlobalMiddleware() ?? [];
|
|
490
|
+
this.gatewayRouteTable?.configure(this.gatewayEntrypoint(), this.gatewayKeyBy());
|
|
491
|
+
this.logger.info("Registering controllers", { controllerCount: controllers.length });
|
|
492
|
+
if (globalMiddleware.length > 0) this.app.use("*", createMiddlewareChain(globalMiddleware));
|
|
493
|
+
if (controllers.some(isGateway)) {
|
|
494
|
+
const { upgradeWebSocket } = await import("hono/cloudflare-workers");
|
|
495
|
+
this.upgradeWebSocketFn = upgradeWebSocket;
|
|
496
|
+
}
|
|
497
|
+
const actions = /* @__PURE__ */ new WeakMap();
|
|
498
|
+
for (const ControllerClass of controllers) this.collectRoutes(ControllerClass, actions);
|
|
499
|
+
for (const route of this.registry.all()) actions.get(route)?.();
|
|
500
|
+
this.logger.info("Controller registration complete");
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Pass 1: Collect routes from a controller into RouteRegistry and store Hono actions.
|
|
504
|
+
* Versioning and locale expansion are handled by RouteRegistry.register().
|
|
505
|
+
*/
|
|
506
|
+
collectRoutes(ControllerClass, actions) {
|
|
507
|
+
const isWsGateway = isGateway(ControllerClass);
|
|
508
|
+
const controllerRoute = getControllerRoute(ControllerClass);
|
|
509
|
+
if (!controllerRoute) throw new RouterError(`Controller "${ControllerClass.name}" registration failed: ${isWsGateway ? "Missing @Gateway decorator or route metadata" : "Missing @Controller decorator or route metadata"}`);
|
|
510
|
+
const controllerOpts = getControllerOptions(ControllerClass);
|
|
511
|
+
const controllerGuards = getControllerGuards(ControllerClass)?.guards ?? [];
|
|
512
|
+
const routerConfig = this.routerResolver?.resolveForController(ControllerClass) ?? { middleware: [] };
|
|
513
|
+
const classThrottleMiddleware = Array.from(new Set(getRateLimits(ControllerClass).map(createThrottleMiddleware)));
|
|
514
|
+
const basePath = routerConfig.prefix ? this.joinPaths(routerConfig.prefix, controllerRoute) : controllerRoute;
|
|
515
|
+
const effectiveVersion = controllerOpts?.version ?? routerConfig.version;
|
|
516
|
+
const effectiveDomain = controllerOpts?.domain ?? routerConfig.domain;
|
|
517
|
+
if (isWsGateway) {
|
|
518
|
+
const wsMiddleware = [...routerConfig.middleware, ...classThrottleMiddleware];
|
|
519
|
+
const expandedRoutes = this.registry.register({
|
|
520
|
+
method: "ws",
|
|
521
|
+
basePath,
|
|
522
|
+
version: effectiveVersion,
|
|
523
|
+
domain: effectiveDomain,
|
|
524
|
+
controller: ControllerClass.name,
|
|
525
|
+
action: "ws",
|
|
526
|
+
hidden: routerConfig.hideFromDocs ?? false,
|
|
527
|
+
middleware: wsMiddleware.map((m) => m.name)
|
|
528
|
+
});
|
|
529
|
+
for (const route of expandedRoutes) actions.set(route, () => {
|
|
530
|
+
if (wsMiddleware.length > 0) this.app.use(route.path, createMiddlewareChain(wsMiddleware));
|
|
531
|
+
if (effectiveDomain) {
|
|
532
|
+
const domainHandler = createDomainMiddleware(effectiveDomain);
|
|
533
|
+
this.app.use(route.path, domainHandler);
|
|
534
|
+
this.app.use(`${route.path}/*`, domainHandler);
|
|
535
|
+
}
|
|
536
|
+
this.registerGatewayForPath(ControllerClass, route.path, controllerGuards);
|
|
537
|
+
});
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const className = ControllerClass.name;
|
|
541
|
+
this.controllerClasses.set(className, ControllerClass);
|
|
542
|
+
const prototype = ControllerClass.prototype;
|
|
543
|
+
if (prototype.handle) {
|
|
544
|
+
if (getCacheable(prototype, "handle") || getPurgesCache(prototype, "handle")) throw new ResponseCacheConfigError(`${className}.handle: @Cacheable/@PurgesCache is not supported on wildcard controllers (those implementing \`handle()\`) yet — the decorator would be silently ignored, never firing and never warning, because wildcard routes do not go through per-method cache binding. Remove the decorator, or restructure this route as a decorated method.`);
|
|
545
|
+
const wildcardMiddleware = [...routerConfig.middleware, ...classThrottleMiddleware];
|
|
546
|
+
const expandedRoutes = this.registry.register({
|
|
547
|
+
method: "all",
|
|
548
|
+
basePath,
|
|
549
|
+
version: effectiveVersion,
|
|
550
|
+
domain: effectiveDomain,
|
|
551
|
+
controller: className,
|
|
552
|
+
action: "handle",
|
|
553
|
+
hidden: routerConfig.hideFromDocs ?? false,
|
|
554
|
+
middleware: wildcardMiddleware.map((m) => m.name)
|
|
555
|
+
});
|
|
556
|
+
for (const route of expandedRoutes) actions.set(route, () => {
|
|
557
|
+
if (wildcardMiddleware.length > 0) this.app.use(route.path, createMiddlewareChain(wildcardMiddleware));
|
|
558
|
+
this.registerWildcardRoute(ControllerClass, route.path);
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const decoratedMethods = getRouteDecoratedMethods(ControllerClass);
|
|
563
|
+
if (decoratedMethods.length === 0) throw new RouterError(`Controller "${ControllerClass.name}" registration failed: No route decorators found. Use @Route() or HTTP method decorators (@Get, @Post, etc.) on controller methods.`);
|
|
564
|
+
const methodMetadata = [];
|
|
565
|
+
let hasConvention = false;
|
|
566
|
+
let hasExplicit = false;
|
|
567
|
+
for (const m of decoratedMethods) {
|
|
568
|
+
const meta = getRouteMetadata(prototype, m);
|
|
569
|
+
if (!meta) continue;
|
|
570
|
+
methodMetadata.push({
|
|
571
|
+
method: m,
|
|
572
|
+
meta
|
|
573
|
+
});
|
|
574
|
+
if (meta.type === "convention") hasConvention = true;
|
|
575
|
+
else if (meta.type === "explicit") hasExplicit = true;
|
|
576
|
+
}
|
|
577
|
+
if (hasConvention && hasExplicit) throw new RouterError(`Controller "${ControllerClass.name}" registration failed: Cannot mix @Route() with HTTP method decorators (@Get, @Post, etc.) in the same controller. Use one pattern or the other.`);
|
|
578
|
+
const routerHidden = routerConfig.hideFromDocs;
|
|
579
|
+
const controllerHidden = controllerOpts?.hideFromDocs ?? false;
|
|
580
|
+
const routerName = routerConfig.name;
|
|
581
|
+
const controllerName = controllerOpts?.name;
|
|
582
|
+
const effectiveNamePrefix = routerName && controllerName ? `${routerName}${controllerName}` : routerName ?? controllerName;
|
|
583
|
+
for (const { method: methodName, meta } of methodMetadata) {
|
|
584
|
+
const resolved = this.resolveMethodAndPath(meta, methodName, basePath, className);
|
|
585
|
+
if (!resolved) continue;
|
|
586
|
+
const methodThrottleMiddleware = getRateLimits(prototype, methodName).map(createThrottleMiddleware);
|
|
587
|
+
const effectiveMiddleware = Array.from(/* @__PURE__ */ new Set([
|
|
588
|
+
...routerConfig.middleware,
|
|
589
|
+
...classThrottleMiddleware,
|
|
590
|
+
...methodThrottleMiddleware
|
|
591
|
+
]));
|
|
592
|
+
const middlewareNames = effectiveMiddleware.map((m) => m.name);
|
|
593
|
+
const { httpMethod, fullPath, routeConfig: rawRouteConfig, statusCodeOverride } = resolved;
|
|
594
|
+
let mergedParams = rawRouteConfig.params;
|
|
595
|
+
if (routerConfig.params) {
|
|
596
|
+
const prefixShape = routerConfig.params.shape;
|
|
597
|
+
mergedParams = mergedParams ? object({
|
|
598
|
+
...mergedParams.shape,
|
|
599
|
+
...prefixShape
|
|
600
|
+
}) : routerConfig.params;
|
|
601
|
+
}
|
|
602
|
+
const routeConfig = mergedParams === rawRouteConfig.params ? rawRouteConfig : {
|
|
603
|
+
...rawRouteConfig,
|
|
604
|
+
params: mergedParams
|
|
605
|
+
};
|
|
606
|
+
const hideFromDocs = routeConfig.hideFromDocs ?? routerHidden ?? controllerHidden;
|
|
607
|
+
let routeName;
|
|
608
|
+
if (routeConfig.name) routeName = effectiveNamePrefix ? `${effectiveNamePrefix}${routeConfig.name}` : routeConfig.name;
|
|
609
|
+
else if (meta.type === "convention") {
|
|
610
|
+
const autoName = generateConventionRouteName(basePath, methodName);
|
|
611
|
+
routeName = effectiveNamePrefix ? `${effectiveNamePrefix}${autoName}` : autoName;
|
|
612
|
+
}
|
|
613
|
+
const expandedRoutes = this.registry.register({
|
|
614
|
+
name: routeName,
|
|
615
|
+
method: httpMethod,
|
|
616
|
+
basePath: fullPath,
|
|
617
|
+
version: effectiveVersion,
|
|
618
|
+
domain: effectiveDomain,
|
|
619
|
+
controller: className,
|
|
620
|
+
action: methodName,
|
|
621
|
+
hidden: hideFromDocs,
|
|
622
|
+
middleware: middlewareNames
|
|
623
|
+
});
|
|
624
|
+
const methodGuards = getMethodGuards(prototype, methodName)?.guards ?? [];
|
|
625
|
+
const allGuards = methodGuards.length > 0 ? [...controllerGuards, ...methodGuards] : controllerGuards;
|
|
626
|
+
const responseSchema = httpMethod !== "all" ? this.extractResponseSchema(routeConfig) : null;
|
|
627
|
+
const cacheableOptions = getCacheable(prototype, methodName);
|
|
628
|
+
const purgesCacheOptions = getPurgesCache(prototype, methodName);
|
|
629
|
+
const capturesPayload = needsPayloadCapture(cacheableOptions, purgesCacheOptions);
|
|
630
|
+
const cacheBinding = bindRouteCache(cacheableOptions, purgesCacheOptions, this.responseCacheDefaults(), {
|
|
631
|
+
controller: className,
|
|
632
|
+
method: methodName,
|
|
633
|
+
guarded: allGuards.length > 0,
|
|
634
|
+
routeParams: extractPathParams(fullPath),
|
|
635
|
+
gatewayConfigured: this.gatewayEntrypoint() !== void 0
|
|
636
|
+
});
|
|
637
|
+
if (cacheBinding && (!this.cacheability || !this.responseCache)) throw new ResponseCacheConfigError(`${className}.${methodName}: @Cacheable/@PurgesCache requires ResponseCacheModule to be imported. Without it, the decorator would be silently dropped — the route would register and run normally, just never cache or purge anything, with no warning at boot or at request time. Add \`ResponseCacheModule.forRoot({ defaults: { ttl } })\` to your app's \`imports\`.`);
|
|
638
|
+
if (cacheBinding?.cacheable || cacheBinding?.purges) this.cacheableRouteCount++;
|
|
639
|
+
const partitionBy = cacheBinding?.cacheable?.partitionBy ?? [];
|
|
640
|
+
if (partitionBy.length > 0) this.partitionResolver?.assertKnown(partitionBy, `${className}.${methodName}`);
|
|
641
|
+
const handler = this.createControllerHandler(ControllerClass, methodName, responseSchema, capturesPayload, cacheBinding);
|
|
642
|
+
for (const route of expandedRoutes) {
|
|
643
|
+
if (partitionBy.length > 0) {
|
|
644
|
+
const methods = httpMethod === "all" ? ["GET", "HEAD"] : [httpMethod];
|
|
645
|
+
for (const method of methods) this.gatewayRouteTable?.record(method, route.path, partitionBy);
|
|
646
|
+
}
|
|
647
|
+
actions.set(route, () => {
|
|
648
|
+
if (effectiveDomain) {
|
|
649
|
+
const domainHandler = createDomainMiddleware(effectiveDomain);
|
|
650
|
+
this.app.use(route.path, domainHandler);
|
|
651
|
+
this.app.use(`${route.path}/*`, domainHandler);
|
|
652
|
+
}
|
|
653
|
+
if (allGuards.length > 0) this.logger.info(`Route guards`, {
|
|
654
|
+
controller: className,
|
|
655
|
+
method: httpMethod.toUpperCase(),
|
|
656
|
+
path: route.path,
|
|
657
|
+
methodName,
|
|
658
|
+
guardCount: allGuards.length
|
|
659
|
+
});
|
|
660
|
+
if (httpMethod === "all") {
|
|
661
|
+
this.logger.info(`Registering @All route`, {
|
|
662
|
+
controller: className,
|
|
663
|
+
path: route.path,
|
|
664
|
+
methodName
|
|
665
|
+
});
|
|
666
|
+
if (effectiveMiddleware.length > 0) this.app.use(route.path, createMiddlewareChain(effectiveMiddleware));
|
|
667
|
+
if (allGuards.length > 0) this.app.use(route.path, this.createGuardMiddleware(allGuards));
|
|
668
|
+
this.app.all(route.path, handler);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const isLocaleVariant = route.isLocaleVariant ?? false;
|
|
672
|
+
const body = this.resolveBody(routeConfig);
|
|
673
|
+
const validators = buildRouteValidators({
|
|
674
|
+
params: routeConfig.params,
|
|
675
|
+
query: routeConfig.query,
|
|
676
|
+
body,
|
|
677
|
+
isLocaleVariant
|
|
678
|
+
});
|
|
679
|
+
const wrappedHandler = this.wrapHandlerWithChain(handler, effectiveMiddleware, allGuards);
|
|
680
|
+
this.logger.info(`Registering route`, {
|
|
681
|
+
controller: className,
|
|
682
|
+
method: httpMethod.toUpperCase(),
|
|
683
|
+
path: route.path,
|
|
684
|
+
methodName,
|
|
685
|
+
hidden: route.hidden
|
|
686
|
+
});
|
|
687
|
+
this.app.on(httpMethod.toUpperCase(), [route.path], ...validators, wrappedHandler);
|
|
688
|
+
const metadata = this.mergeMetadata(controllerOpts, routeConfig, ControllerClass, methodName);
|
|
689
|
+
this.metadataRegistry.add(this.buildRouteMetadata({
|
|
690
|
+
method: httpMethod,
|
|
691
|
+
path: route.path,
|
|
692
|
+
routeConfig,
|
|
693
|
+
metadata,
|
|
694
|
+
body,
|
|
695
|
+
methodName: meta.type === "convention" ? methodName : void 0,
|
|
696
|
+
statusCodeOverride,
|
|
697
|
+
isLocaleVariant,
|
|
698
|
+
hidden: route.hidden
|
|
699
|
+
}));
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Register a single WebSocket gateway route
|
|
706
|
+
*/
|
|
707
|
+
registerGatewayForPath(GatewayClass, fullPath, guards) {
|
|
708
|
+
const onMsgMethod = getWsOnMessageMethod(GatewayClass);
|
|
709
|
+
const onCloseMethod = getWsOnCloseMethod(GatewayClass);
|
|
710
|
+
const onErrMethod = getWsOnErrorMethod(GatewayClass);
|
|
711
|
+
const wsHandler = this.upgradeWebSocketFn((c) => {
|
|
712
|
+
const gateway = new RouterContext(c).getContainer().resolve(GatewayClass);
|
|
713
|
+
const events = {};
|
|
714
|
+
const bindWsHandler = (method, onCatch) => {
|
|
715
|
+
return (evt, ws) => {
|
|
716
|
+
const ctx = new GatewayContext(c, ws);
|
|
717
|
+
invokeHandler(gateway, method, evt, ctx).catch((err) => {
|
|
718
|
+
this.logger.error(`WebSocket ${method} handler error`, err, { gateway: GatewayClass.name });
|
|
719
|
+
onCatch?.(err, ws);
|
|
720
|
+
});
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
if (onMsgMethod) events.onMessage = bindWsHandler(onMsgMethod, (_err, ws) => ws.close(1011, "Internal Error"));
|
|
724
|
+
if (onCloseMethod) events.onClose = bindWsHandler(onCloseMethod);
|
|
725
|
+
else events.onClose = (_evt, ws) => {
|
|
726
|
+
ws.close();
|
|
727
|
+
};
|
|
728
|
+
if (onErrMethod) events.onError = bindWsHandler(onErrMethod);
|
|
729
|
+
return events;
|
|
730
|
+
});
|
|
731
|
+
this.nameHandler(wsHandler, GatewayClass.name, onMsgMethod ?? "[anonymous]", "ws");
|
|
732
|
+
this.logger.info("Registering WebSocket gateway", {
|
|
733
|
+
gateway: GatewayClass.name,
|
|
734
|
+
path: fullPath
|
|
735
|
+
});
|
|
736
|
+
const handlers = [];
|
|
737
|
+
if (guards.length > 0) {
|
|
738
|
+
this.logger.info("Gateway guards", {
|
|
739
|
+
gateway: GatewayClass.name,
|
|
740
|
+
path: fullPath,
|
|
741
|
+
guardCount: guards.length
|
|
742
|
+
});
|
|
743
|
+
handlers.push(this.createGuardMiddleware(guards));
|
|
744
|
+
}
|
|
745
|
+
handlers.push(wsHandler);
|
|
746
|
+
this.app.get(fullPath, ...handlers);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Create a guard execution middleware
|
|
750
|
+
*
|
|
751
|
+
* This middleware executes all guards for a route before the handler.
|
|
752
|
+
* Guards are executed in order; all must pass for the request to proceed.
|
|
753
|
+
*
|
|
754
|
+
* @param guards - Array of guards to execute
|
|
755
|
+
* @returns Hono middleware function
|
|
756
|
+
*/
|
|
757
|
+
createGuardMiddleware(guards) {
|
|
758
|
+
const guardService = new GuardExecutionService(this.logger);
|
|
759
|
+
return async (c, next) => {
|
|
760
|
+
const ctx = new RouterContext(c);
|
|
761
|
+
const container = ctx.getContainer();
|
|
762
|
+
await guardService.executeGuards(guards, ctx, container);
|
|
763
|
+
await next();
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Wrap a controller handler with a `scopedMiddleware → guards → handler`
|
|
768
|
+
* chain that runs *inside* the Hono route handler — after request
|
|
769
|
+
* validators have populated `c.req.valid(...)`. This is the only place
|
|
770
|
+
* we can run user middleware after `@hono/zod-openapi`'s validators in
|
|
771
|
+
* the same pipeline.
|
|
772
|
+
*
|
|
773
|
+
* Returns a Hono handler with the same signature as the original so
|
|
774
|
+
* `app.openapi(route, wrapped)` works transparently.
|
|
775
|
+
*/
|
|
776
|
+
wrapHandlerWithChain(handler, scopedMiddleware, guards) {
|
|
777
|
+
if (scopedMiddleware.length === 0 && guards.length === 0) return handler;
|
|
778
|
+
const scopedChain = scopedMiddleware.length > 0 ? createMiddlewareChain(scopedMiddleware) : null;
|
|
779
|
+
const guardChain = guards.length > 0 ? this.createGuardMiddleware(guards) : null;
|
|
780
|
+
return async (c) => {
|
|
781
|
+
let captured;
|
|
782
|
+
const runHandler = async () => {
|
|
783
|
+
captured = await handler(c);
|
|
784
|
+
};
|
|
785
|
+
const runGuards = guardChain ? () => guardChain(c, runHandler) : runHandler;
|
|
786
|
+
const result = await (scopedChain ? () => scopedChain(c, runGuards) : runGuards)();
|
|
787
|
+
if (result instanceof Response) return result;
|
|
788
|
+
return captured;
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Register wildcard route for non-RESTful controllers
|
|
793
|
+
*/
|
|
794
|
+
registerWildcardRoute(ControllerClass, route) {
|
|
795
|
+
this.logger.info(`Registering wildcard route`, {
|
|
796
|
+
controller: ControllerClass.name,
|
|
797
|
+
route: `${route}/:path{.+}`,
|
|
798
|
+
method: "ALL"
|
|
799
|
+
});
|
|
800
|
+
const handler = this.createControllerHandler(ControllerClass, "handle");
|
|
801
|
+
this.app.all(route, handler);
|
|
802
|
+
this.app.all(`${route}/:path{.+}`, handler);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Resolve HTTP method, path, route config, and status code from route metadata.
|
|
806
|
+
*/
|
|
807
|
+
resolveMethodAndPath(meta, methodName, basePath, className) {
|
|
808
|
+
if (meta.type === "convention") {
|
|
809
|
+
const derived = this.deriveHttpMethodAndPath(methodName, basePath);
|
|
810
|
+
if (!derived) throw new RouterError(`Cannot derive HTTP method/path for convention-based route "${className}.${methodName}". Ensure the method name follows the naming convention (e.g., index, create, show).`);
|
|
811
|
+
return {
|
|
812
|
+
httpMethod: derived.method,
|
|
813
|
+
fullPath: derived.path,
|
|
814
|
+
routeConfig: meta.config,
|
|
815
|
+
statusCodeOverride: meta.config.statusCode
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
httpMethod: meta.method,
|
|
820
|
+
fullPath: this.joinPaths(basePath, meta.path),
|
|
821
|
+
routeConfig: meta.config,
|
|
822
|
+
statusCodeOverride: meta.config.statusCode
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Join a base path and a route path, normalizing slashes
|
|
827
|
+
*/
|
|
828
|
+
joinPaths(basePath, routePath) {
|
|
829
|
+
if (basePath.endsWith("/")) basePath = basePath.slice(0, -1);
|
|
830
|
+
if (routePath === "/" || routePath === "") return basePath || "/";
|
|
831
|
+
if (!routePath.startsWith("/")) routePath = "/" + routePath;
|
|
832
|
+
return basePath + routePath;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Auto-derive HTTP method and path from controller method name
|
|
836
|
+
* Uses HTTP_METHODS constant for RESTful convention mapping
|
|
837
|
+
*/
|
|
838
|
+
deriveHttpMethodAndPath(methodName, basePath) {
|
|
839
|
+
if (!(methodName in HTTP_METHODS)) return null;
|
|
840
|
+
const mapping = HTTP_METHODS[methodName];
|
|
841
|
+
return {
|
|
842
|
+
method: mapping.method,
|
|
843
|
+
path: basePath + mapping.path
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Merge controller-level and route-level metadata
|
|
848
|
+
* Tags are merged (appended), security is merged (union)
|
|
849
|
+
* Guards automatically add sessionCookie security if present
|
|
850
|
+
*/
|
|
851
|
+
mergeMetadata(controllerOpts, routeConfig, ControllerClass, methodName) {
|
|
852
|
+
const tags = [...controllerOpts?.tags ?? [], ...routeConfig.tags ?? []];
|
|
853
|
+
const groups = [...controllerOpts?.groups ?? [], ...routeConfig.groups ?? []];
|
|
854
|
+
const prototype = ControllerClass.prototype;
|
|
855
|
+
const hasMethodGuards = (getMethodGuards(prototype, methodName)?.guards.length ?? 0) > 0;
|
|
856
|
+
const hasControllerGuards = (getControllerGuards(ControllerClass)?.guards.length ?? 0) > 0;
|
|
857
|
+
const requiresAuth = hasMethodGuards || hasControllerGuards;
|
|
858
|
+
let security = [];
|
|
859
|
+
if (routeConfig.security !== void 0) security = [...controllerOpts?.security ?? [], ...routeConfig.security];
|
|
860
|
+
else if (controllerOpts?.security) security = controllerOpts.security;
|
|
861
|
+
if (requiresAuth && !security.includes(SECURITY_SCHEMES.SESSION_COOKIE)) security.push(SECURITY_SCHEMES.SESSION_COOKIE);
|
|
862
|
+
return {
|
|
863
|
+
tags,
|
|
864
|
+
security: security.length > 0 ? security.map((scheme) => ({ [scheme]: [] })) : [],
|
|
865
|
+
groups
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Resolve a route's request body into its schema + content type, normalizing
|
|
870
|
+
* the bare-schema and `{ schema, contentType }` forms. Returns `undefined`
|
|
871
|
+
* when no body is declared. Shared by validator construction and metadata.
|
|
872
|
+
*/
|
|
873
|
+
resolveBody(routeConfig) {
|
|
874
|
+
if (!routeConfig.body) return void 0;
|
|
875
|
+
return {
|
|
876
|
+
schema: this.isRouteBodyObject(routeConfig.body) ? routeConfig.body.schema : routeConfig.body,
|
|
877
|
+
contentType: this.isRouteBodyObject(routeConfig.body) ? routeConfig.body.contentType ?? "application/json" : DEFAULT_CONTENT_TYPE
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Build the schema metadata entry for a route. Consumed lazily by the OpenAPI
|
|
882
|
+
* generator — common error responses are added there, not here, so
|
|
883
|
+
* `common.schemas` (and the schemas it references) never reach the routing
|
|
884
|
+
* path. The `locale` path param of a localized route is recorded as plain
|
|
885
|
+
* metadata (enforced by the route pattern, read via `getLocale()`), so no zod
|
|
886
|
+
* enum is constructed at registration.
|
|
887
|
+
*/
|
|
888
|
+
buildRouteMetadata(input) {
|
|
889
|
+
const { routeConfig, metadata } = input;
|
|
890
|
+
const successStatus = input.statusCodeOverride ?? (input.methodName ? METHOD_STATUS_CODES[input.methodName] : void 0) ?? 200;
|
|
891
|
+
const responses = [];
|
|
892
|
+
const responseDef = routeConfig.response;
|
|
893
|
+
if (responseDef && this.isRouteResponseObject(responseDef)) responses.push({
|
|
894
|
+
status: successStatus,
|
|
895
|
+
schema: responseDef.schema,
|
|
896
|
+
contentType: responseDef.contentType ?? "application/json",
|
|
897
|
+
description: responseDef.description ?? `Response ${successStatus}`
|
|
898
|
+
});
|
|
899
|
+
else responses.push({
|
|
900
|
+
status: successStatus,
|
|
901
|
+
schema: responseDef,
|
|
902
|
+
contentType: DEFAULT_CONTENT_TYPE,
|
|
903
|
+
description: `Response ${successStatus}`
|
|
904
|
+
});
|
|
905
|
+
const localeConfig = this.localePathService.localePathConfig;
|
|
906
|
+
const localeParam = input.isLocaleVariant && localeConfig ? {
|
|
907
|
+
name: "locale",
|
|
908
|
+
values: localeConfig.prefixedLocales
|
|
909
|
+
} : void 0;
|
|
910
|
+
const entry = {
|
|
911
|
+
method: input.method,
|
|
912
|
+
path: toOpenAPIPath(input.path),
|
|
913
|
+
hidden: input.hidden ?? false,
|
|
914
|
+
tags: metadata.tags,
|
|
915
|
+
security: metadata.security,
|
|
916
|
+
request: {
|
|
917
|
+
params: routeConfig.params,
|
|
918
|
+
query: routeConfig.query,
|
|
919
|
+
body: input.body
|
|
920
|
+
},
|
|
921
|
+
responses,
|
|
922
|
+
localeParam
|
|
923
|
+
};
|
|
924
|
+
if (routeConfig.summary) entry.summary = routeConfig.summary;
|
|
925
|
+
if (routeConfig.description) entry.description = routeConfig.description;
|
|
926
|
+
if (metadata.groups.length > 0) entry.groups = metadata.groups;
|
|
927
|
+
return entry;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Check if a body definition is a RouteBodyObject (has schema key) vs bare ZodType
|
|
931
|
+
*/
|
|
932
|
+
isRouteBodyObject(body) {
|
|
933
|
+
return typeof body === "object" && "schema" in body;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Resolve method parameter injections from the container
|
|
937
|
+
*
|
|
938
|
+
* @param prototype - Controller prototype
|
|
939
|
+
* @param methodName - Method name to get injections for
|
|
940
|
+
* @param container - Request-scoped container
|
|
941
|
+
* @returns Array of resolved dependencies in parameter order
|
|
942
|
+
*/
|
|
943
|
+
resolveMethodInjections(prototype, methodName, container) {
|
|
944
|
+
const injections = getMethodInjections(prototype, methodName);
|
|
945
|
+
if (!injections.length) return [];
|
|
946
|
+
return injections.map((inj) => container.resolve(inj.token));
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Name a handler function so Hono's inspectRoutes() can identify it.
|
|
950
|
+
* Format: `{type}:{Controller}.{method}` (e.g. `http:UsersController.create`)
|
|
951
|
+
*/
|
|
952
|
+
nameHandler(fn, controller, method, type = "http") {
|
|
953
|
+
Object.defineProperty(fn, "name", { value: `${type}:${controller}.${method}` });
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Create controller handler that resolves controller from request-scoped container
|
|
957
|
+
* This ensures each request gets a fresh controller with request-scoped context
|
|
958
|
+
*/
|
|
959
|
+
createControllerHandler(ControllerClass, methodName, responseSchema = null, capturesPayload = false, cacheBinding) {
|
|
960
|
+
const handler = async (c) => {
|
|
961
|
+
const override = c.get("validationSuccessResponse");
|
|
962
|
+
if (override) return override;
|
|
963
|
+
const ctx = new RouterContext(c);
|
|
964
|
+
const requestContainer = ctx.getContainer();
|
|
965
|
+
const controller = requestContainer.resolve(ControllerClass);
|
|
966
|
+
const method = controller[methodName];
|
|
967
|
+
if (typeof method === "function") {
|
|
968
|
+
const injectedArgs = this.resolveMethodInjections(ControllerClass.prototype, methodName, requestContainer);
|
|
969
|
+
const response = await method.apply(controller, [ctx, ...injectedArgs]);
|
|
970
|
+
if (capturesPayload) await capturePayload(c, response);
|
|
971
|
+
const validated = responseSchema && c.env.ENVIRONMENT !== "production" ? await this.validateResponse(response, responseSchema) : response;
|
|
972
|
+
return this.applyCacheDecision(c, validated, cacheBinding, {
|
|
973
|
+
controller: ControllerClass.name,
|
|
974
|
+
method: methodName
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
throw new RouterError(`Method "${methodName}" not found on controller "${ControllerClass.name}"`);
|
|
978
|
+
};
|
|
979
|
+
this.nameHandler(handler, ControllerClass.name, methodName);
|
|
980
|
+
return handler;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Apply this route's `@Cacheable`/`@PurgesCache` decision, if it has one.
|
|
984
|
+
*
|
|
985
|
+
* Deliberately does **not** stamp `no-store` for the "nothing to do" case
|
|
986
|
+
* (`!cacheBinding`, or `cacheBinding.cacheable` absent) — the outermost
|
|
987
|
+
* `createNoStoreFallbackMiddleware` (registered in `HonoApp`) catches every
|
|
988
|
+
* response that reaches Hono without a `Cache-Control`, this one included,
|
|
989
|
+
* so every response still gets an explicit decision without every call
|
|
990
|
+
* site needing to know that. This function only needs to handle the
|
|
991
|
+
* positive cases: computing `@Cacheable` headers, and firing `@PurgesCache`.
|
|
992
|
+
*
|
|
993
|
+
* `@PurgesCache` fires independently of `@Cacheable` — the common case is
|
|
994
|
+
* a mutation route (create/update/destroy) that isn't itself cacheable but
|
|
995
|
+
* must invalidate a GET route's cached entries on success. Gating the
|
|
996
|
+
* purge on `cacheBinding.cacheable` would silently skip that purge.
|
|
997
|
+
*
|
|
998
|
+
* **Headers stamped here are not the last word.** This runs *inside* the
|
|
999
|
+
* Hono route handler, so any outbound middleware — anything doing work
|
|
1000
|
+
* after its own `await next()` — sees the response afterwards and can
|
|
1001
|
+
* overwrite `Vary`, `Cache-Control`, or `Set-Cookie` with a plain
|
|
1002
|
+
* `c.header(name, value)`, which replaces rather than appends. That is
|
|
1003
|
+
* silent: the route still looks cached, but (for `Vary`) every variant
|
|
1004
|
+
* collapses onto one entry. Middleware that contributes to any of these
|
|
1005
|
+
* three headers must **merge** with what is already on the response, never
|
|
1006
|
+
* set it outright — see `InertiaMiddleware`'s `Vary: X-Inertia` handling
|
|
1007
|
+
* for the shape that fix takes.
|
|
1008
|
+
*/
|
|
1009
|
+
async applyCacheDecision(c, response, cacheBinding, routeContext) {
|
|
1010
|
+
if (!cacheBinding) return response;
|
|
1011
|
+
let scopes;
|
|
1012
|
+
let result = response;
|
|
1013
|
+
if (cacheBinding.cacheable && this.cachingAvailable(c)) {
|
|
1014
|
+
if (!this.cacheability) throw new ResponseCacheConfigError("internal error: a route resolved a `@Cacheable` binding but CacheabilityService is unavailable. This should have been caught at registration — please file a bug.");
|
|
1015
|
+
scopes = this.buildTagScopes(c);
|
|
1016
|
+
result = this.cacheability.apply(result, cacheBinding.cacheable, scopes, {
|
|
1017
|
+
partitionsResolved: this.partitionsResolved(c, cacheBinding.cacheable.partitionBy),
|
|
1018
|
+
inertia: c.get("inertiaCacheSignals")
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
if (cacheBinding.purges && result.status >= 200 && result.status < 400) {
|
|
1022
|
+
if (!this.responseCache) throw new ResponseCacheConfigError("internal error: a route resolved a `@PurgesCache` binding but ResponseCacheService is unavailable. This should have been caught at registration — please file a bug.");
|
|
1023
|
+
scopes ??= this.buildTagScopes(c);
|
|
1024
|
+
const cache = this.executionCache(c);
|
|
1025
|
+
if (!cache) return result;
|
|
1026
|
+
let spec;
|
|
1027
|
+
try {
|
|
1028
|
+
spec = this.responseCache.buildPurgeSpec(cacheBinding.purges, scopes);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1031
|
+
this.logger.error("[stratal:response-cache] Failed to render @PurgesCache tags", {
|
|
1032
|
+
controller: routeContext.controller,
|
|
1033
|
+
action: routeContext.method,
|
|
1034
|
+
path: c.req.routePath,
|
|
1035
|
+
error: detail
|
|
1036
|
+
});
|
|
1037
|
+
throw new CachePurgeError(detail, error);
|
|
1038
|
+
}
|
|
1039
|
+
await this.responseCache.purge(spec, cache);
|
|
1040
|
+
}
|
|
1041
|
+
return result;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Whether this request's declared partitions are actually in the cache key.
|
|
1045
|
+
*
|
|
1046
|
+
* Reaching a controller **inline while running as the gateway** is, for a
|
|
1047
|
+
* route with a non-empty `partitionBy`, proof that they are not: the
|
|
1048
|
+
* dispatch middleware forwards every such `GET`/`HEAD` whose partitions it
|
|
1049
|
+
* could resolve, so the only way one arrives here is that it declined —
|
|
1050
|
+
* an unresolved resolver, a resolver that threw, or a primer that
|
|
1051
|
+
* short-circuited. Reporting `true` there would stamp
|
|
1052
|
+
* `Cache-Control: public, max-age=…` on a response the author declared
|
|
1053
|
+
* per-caller, and while the gateway's own Workers cache is disabled, that
|
|
1054
|
+
* header still travels to the client and to every intermediary between
|
|
1055
|
+
* them. One user's dashboard in a shared proxy cache is exactly the leak
|
|
1056
|
+
* `partitionBy` exists to prevent, so it fails closed instead.
|
|
1057
|
+
*
|
|
1058
|
+
* Inside the cached entrypoint the partitions normally *are* in the key,
|
|
1059
|
+
* because the gateway chose `ctx.props` before forwarding. But that is the
|
|
1060
|
+
* caller's claim, not a fact, and this entrypoint is reachable by anything
|
|
1061
|
+
* that can name it — another Worker's service binding, a misrouted export,
|
|
1062
|
+
* or our own gateway having selected an overlapping route's binding. So it
|
|
1063
|
+
* is checked rather than trusted: every partition this route declared must
|
|
1064
|
+
* be present in `ctx.props`. A caller that supplies none, or only some, gets
|
|
1065
|
+
* `private, no-store` instead of a shared entry.
|
|
1066
|
+
*
|
|
1067
|
+
* With **no execution context at all** — `hono.fetch(request, env)`, which
|
|
1068
|
+
* `quarry api` and `mcp serve` both do — there is no way to tell those cases
|
|
1069
|
+
* apart, so unknown means uncacheable.
|
|
1070
|
+
*
|
|
1071
|
+
* An app with no gateway configured can never have a non-empty
|
|
1072
|
+
* `partitionBy` (`bindRouteCache` rejects it at boot), so this returns
|
|
1073
|
+
* `true` on the first line for every app that doesn't use the feature.
|
|
1074
|
+
*/
|
|
1075
|
+
partitionsResolved(c, partitionBy) {
|
|
1076
|
+
if (partitionBy.length === 0) return true;
|
|
1077
|
+
let ctx;
|
|
1078
|
+
try {
|
|
1079
|
+
ctx = c.executionCtx;
|
|
1080
|
+
} catch {
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
if (isGatewayMode(ctx)) return false;
|
|
1084
|
+
const props = ctx.props;
|
|
1085
|
+
return partitionBy.every((name) => props?.[name] !== void 0);
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* `c.executionCtx` is a Hono getter that throws when the context wasn't
|
|
1089
|
+
* constructed with one (e.g. a bare `app.fetch(request)` in a unit test) —
|
|
1090
|
+
* unlike a plain optional property, there's no way to read it without a
|
|
1091
|
+
* `try`/`catch` when it might be absent. `cache` itself is optional even
|
|
1092
|
+
* when `executionCtx` exists: Wrangler only attaches it when `cache.enabled
|
|
1093
|
+
* = true` is configured.
|
|
1094
|
+
*
|
|
1095
|
+
* The cast is necessary despite `@cloudflare/workers-types` declaring
|
|
1096
|
+
* `cache?: CacheContext` on the *runtime* `ExecutionContext` — Hono's
|
|
1097
|
+
* `Context.executionCtx` getter is typed against Hono's own narrower
|
|
1098
|
+
* `ExecutionContext` interface (`waitUntil`/`passThroughOnException`/`props`
|
|
1099
|
+
* only), not Cloudflare's, so TypeScript doesn't know `.cache` exists on
|
|
1100
|
+
* what it returns even though the object handed to it at runtime does.
|
|
1101
|
+
*
|
|
1102
|
+
* **In gateway mode with a gateway configured, this is deliberately not
|
|
1103
|
+
* `ctx.cache`.** Only partitioned `GET`/`HEAD` requests are forwarded to the
|
|
1104
|
+
* cached entrypoint; every mutation — and so every `@PurgesCache` — runs
|
|
1105
|
+
* inline in the gateway, whose Wrangler config sets `cache: { enabled:
|
|
1106
|
+
* false }`. Purges are scoped to the entrypoint that issues them, so an
|
|
1107
|
+
* inline purge would either find no `ctx.cache` at all (and 500 a mutation
|
|
1108
|
+
* that already committed) or, if caching were left on for the gateway, purge
|
|
1109
|
+
* the gateway's own cache — reporting success while invalidating nothing and
|
|
1110
|
+
* leaving every cached read stale until its TTL ran out. Routing it over RPC
|
|
1111
|
+
* to the cached entrypoint puts the purge where the entries actually live.
|
|
1112
|
+
*/
|
|
1113
|
+
/**
|
|
1114
|
+
* Whether the entrypoint serving this request can cache.
|
|
1115
|
+
*
|
|
1116
|
+
* Reported once per entrypoint, at error level, because an app that declares
|
|
1117
|
+
* `@Cacheable` and gets no caching is misconfigured somewhere — in a
|
|
1118
|
+
* deployment that omitted `cache.enabled`, or in a local runtime that has no
|
|
1119
|
+
* Workers Caching to offer. Either way its routes then serve uncached and
|
|
1120
|
+
* say so in the response, rather than advertising a freshness nothing keeps.
|
|
1121
|
+
*/
|
|
1122
|
+
cachingAvailable(c) {
|
|
1123
|
+
const gateway = this.isGatewayRequest(c);
|
|
1124
|
+
const known = this.cachingAvailability.get(gateway);
|
|
1125
|
+
if (known !== void 0) return known;
|
|
1126
|
+
const reason = cachingUnavailableReason(this.cacheableRouteCount, this.executionCache(c));
|
|
1127
|
+
this.cachingAvailability.set(gateway, reason === void 0);
|
|
1128
|
+
if (reason !== void 0) this.logger.error(`[stratal:response-cache] ${reason}`);
|
|
1129
|
+
return reason === void 0;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Whether this request came in through the gateway rather than the cached
|
|
1133
|
+
* entrypoint. Both run the same Hono app over the same isolate, and only the
|
|
1134
|
+
* execution context tells them apart.
|
|
1135
|
+
*/
|
|
1136
|
+
isGatewayRequest(c) {
|
|
1137
|
+
try {
|
|
1138
|
+
return isGatewayMode(c.executionCtx);
|
|
1139
|
+
} catch {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
executionCache(c) {
|
|
1144
|
+
let ctx;
|
|
1145
|
+
try {
|
|
1146
|
+
ctx = c.executionCtx;
|
|
1147
|
+
} catch {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const entrypoint = this.gatewayEntrypoint();
|
|
1151
|
+
if (entrypoint !== void 0 && isGatewayMode(ctx)) return createLoopbackPurgeTarget(ctx, entrypoint);
|
|
1152
|
+
return ctx.cache;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Values available to `{scope.path}` cache tag templates for this request.
|
|
1156
|
+
*
|
|
1157
|
+
* `body` is deliberately `undefined`: the parsed request body isn't cheaply
|
|
1158
|
+
* available at this point in the pipeline, and re-parsing it here would add
|
|
1159
|
+
* a second parse to every request on the hot path just to support a rarely
|
|
1160
|
+
* used tag scope. A `{body.*}` tag can therefore never resolve — but that's
|
|
1161
|
+
* never discovered here, or at request time at all: `bindRouteCache`
|
|
1162
|
+
* rejects any `@Cacheable`/`@PurgesCache` tag using the `body` scope at
|
|
1163
|
+
* route registration, so a route that reaches this method never has one to
|
|
1164
|
+
* render. (It was never true that both services "fail closed" on this —
|
|
1165
|
+
* `CacheabilityService` does, but `@PurgesCache`'s `renderTags` call has no
|
|
1166
|
+
* surrounding try/catch, so letting a `{body.*}` tag through would 500 the
|
|
1167
|
+
* request after its mutation already committed, which is exactly why the
|
|
1168
|
+
* boot-time check exists.)
|
|
1169
|
+
*/
|
|
1170
|
+
buildTagScopes(c) {
|
|
1171
|
+
return {
|
|
1172
|
+
param: c.req.param(),
|
|
1173
|
+
query: c.req.query(),
|
|
1174
|
+
body: void 0,
|
|
1175
|
+
data: c.get(ROUTER_CONTEXT_KEYS.RESPONSE_PAYLOAD),
|
|
1176
|
+
partition: this.partitionValues(c)
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* The partition values Cloudflare is keying this response on.
|
|
1181
|
+
*
|
|
1182
|
+
* Read from `ctx.props`, which is where the gateway put them and what the
|
|
1183
|
+
* cache key is actually built from — so a `{partition.x}` tag names the same
|
|
1184
|
+
* value the entry is stored under, on every variant of it.
|
|
1185
|
+
*
|
|
1186
|
+
* Empty when there is no execution context, or when running as the gateway
|
|
1187
|
+
* (whose own responses are never stored). A tag naming a partition that is
|
|
1188
|
+
* absent then fails to render, which fails the response closed — the right
|
|
1189
|
+
* outcome, since the partition it claimed to be keyed by is not in the key.
|
|
1190
|
+
*/
|
|
1191
|
+
partitionValues(c) {
|
|
1192
|
+
let ctx;
|
|
1193
|
+
try {
|
|
1194
|
+
ctx = c.executionCtx;
|
|
1195
|
+
} catch {
|
|
1196
|
+
return {};
|
|
1197
|
+
}
|
|
1198
|
+
if (isGatewayMode(ctx)) return {};
|
|
1199
|
+
const values = {};
|
|
1200
|
+
for (const [name, value] of Object.entries(ctx.props ?? {})) if (typeof value === "string") values[name] = value;
|
|
1201
|
+
return values;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Extract the Zod schema from a RouteResponse definition.
|
|
1205
|
+
* Returns null for non-JSON content types or when no response is defined.
|
|
1206
|
+
*/
|
|
1207
|
+
extractResponseSchema(routeConfig) {
|
|
1208
|
+
const responseDef = routeConfig.response;
|
|
1209
|
+
if (!responseDef) return null;
|
|
1210
|
+
if (this.isRouteResponseObject(responseDef)) {
|
|
1211
|
+
if (!(responseDef.contentType ?? "application/json").includes("application/json")) return null;
|
|
1212
|
+
return responseDef.schema;
|
|
1213
|
+
}
|
|
1214
|
+
return responseDef;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Check if a response definition is a RouteResponseObject (has schema key) vs bare ZodType
|
|
1218
|
+
*/
|
|
1219
|
+
isRouteResponseObject(response) {
|
|
1220
|
+
return typeof response === "object" && "schema" in response;
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Validate a Response body against its declared Zod schema.
|
|
1224
|
+
*
|
|
1225
|
+
* Skips validation for:
|
|
1226
|
+
* - Non-JSON content types
|
|
1227
|
+
* - Empty bodies (204 No Content, 304 Not Modified)
|
|
1228
|
+
*
|
|
1229
|
+
* Clones the response to read the body without consuming the original stream.
|
|
1230
|
+
*/
|
|
1231
|
+
async validateResponse(response, schema) {
|
|
1232
|
+
const contentType = response.headers.get("content-type");
|
|
1233
|
+
if (!contentType || !contentType.includes("application/json")) return response;
|
|
1234
|
+
if (response.status === 204 || response.status === 304) return response;
|
|
1235
|
+
const cloned = response.clone();
|
|
1236
|
+
let body;
|
|
1237
|
+
try {
|
|
1238
|
+
body = await cloned.json();
|
|
1239
|
+
} catch {
|
|
1240
|
+
return response;
|
|
1241
|
+
}
|
|
1242
|
+
const result = await schema.safeParseAsync(body);
|
|
1243
|
+
if (!result.success) throw new ResponseValidationError(result.error);
|
|
1244
|
+
return response;
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
RouteRegistrationService = __decorate([
|
|
1248
|
+
Singleton(),
|
|
1249
|
+
__decorateParam(0, inject(LOGGER_TOKENS.LoggerService)),
|
|
1250
|
+
__decorateParam(1, inject(ROUTER_TOKENS.RouteRegistry)),
|
|
1251
|
+
__decorateParam(2, inject(ROUTER_TOKENS.RouterResolver, { isOptional: true })),
|
|
1252
|
+
__decorateParam(3, inject(ROUTER_TOKENS.LocalePathService)),
|
|
1253
|
+
__decorateParam(4, inject(ROUTER_TOKENS.HonoApp)),
|
|
1254
|
+
__decorateParam(5, inject(DI_TOKENS.ModuleRegistry)),
|
|
1255
|
+
__decorateParam(6, inject(ROUTER_TOKENS.RouteMetadataRegistry)),
|
|
1256
|
+
__decorateParam(7, inject(RESPONSE_CACHE_TOKENS.CacheabilityService, { isOptional: true })),
|
|
1257
|
+
__decorateParam(8, inject(RESPONSE_CACHE_TOKENS.ResponseCacheService, { isOptional: true })),
|
|
1258
|
+
__decorateParam(9, inject(RESPONSE_CACHE_TOKENS.Options, { isOptional: true })),
|
|
1259
|
+
__decorateParam(10, inject(RESPONSE_CACHE_TOKENS.GatewayRouteTable, { isOptional: true })),
|
|
1260
|
+
__decorateParam(11, inject(RESPONSE_CACHE_TOKENS.PartitionResolverService, { isOptional: true }))
|
|
1261
|
+
], RouteRegistrationService);
|
|
1262
|
+
//#endregion
|
|
1263
|
+
export { getRouteMetadata as a, parseDomainPattern as c, SchemaValidationError as d, getRouteDecoratedMethods as i, InvalidSignatureError as l, route_registration_service_exports as n, createMiddlewareChain as o, Route as r, createDomainMiddleware as s, RouteRegistrationService as t, ResponseValidationError as u };
|
|
1264
|
+
|
|
1265
|
+
//# sourceMappingURL=route-registration.service-COKBZTsn.mjs.map
|