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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/rate-limiter/errors.ts","../../src/rate-limiter/limit.ts","../../src/rate-limiter/rate-limiter-registry.ts","../../src/rate-limiter/stores/rate-limiter-store.interface.ts","../../src/rate-limiter/stores/store-factory.ts","../../src/rate-limiter/rate-limiter.module.ts","../../src/rate-limiter/rate-limiter.tokens.ts","../../src/rate-limiter/throttle.middleware.ts","../../src/rate-limiter/decorators/rate-limit.decorator.ts","../../src/rate-limiter/stores/kv-store.ts","../../src/rate-limiter/stores/memory-store.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/rate-limiter/errors.ts","../../src/rate-limiter/limit.ts","../../src/rate-limiter/rate-limiter-registry.ts","../../src/rate-limiter/stores/rate-limiter-store.interface.ts","../../src/rate-limiter/stores/store-factory.ts","../../src/rate-limiter/rate-limiter.module.ts","../../src/rate-limiter/rate-limiter.tokens.ts","../../src/rate-limiter/throttle.middleware.ts","../../src/rate-limiter/decorators/rate-limit.decorator.ts","../../src/rate-limiter/stores/kv-store.ts","../../src/rate-limiter/stores/memory-store.ts"],"mappings":";;;;;;;;;;;;;;;;qBAYa,6BAA6B;WAEtB;IAAQ;IAAoB;IAAe;;EAD7D,YACkB;IAAQ;IAAoB;IAAe;;;qBAMlD,yBAAyB;;;;;;;UCdrB;EACf;EACA;EACA;EACA;;;;;;;;KAsBU,4BACV,KAAK,eACL,SAAS,qBACN,WAAW,QAAQ;;;;;;;;;;;;;;;;;;qBAmBX;WAMO;WACA;WACA;UAPV;UACA;UACA;UAED;SAMA,UAAU,cAAc;SAIxB,WAAW,iBAAiB,cAAc;SAI1C,UAAU,cAAc;SAIxB,WAAW,iBAAiB,cAAc;SAI1C,QAAQ,cAAc;SAItB,OAAO,cAAc;;SAKrB,QAAQ;;EAKf,GAAG;;;;;;EAUH,SAAS,SAAS;;;;;;;;;;;;;;;;;;;;;EAyBlB,WAAW;MAKP;MAIA,kBAAkB;MAIlB;;;;;;;;;KChIM,iBACV,KAAK,kBACF,QAAQ,UAAU,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;qBAuC1B,4BAA4B;mBAIS;mBAH/B;EAEjB,YACgD,WAAW;;;;;;;cAW/C;;;;;;EASZ,IAAI,cAAc,UAAU;EAI5B,IAAI;;;;;;;;;EAYE,OAAO,cAAc,KAAK,eAAe,MAAM,OAAO,QAAQ;;;;;UAyEtD;;;;;;;;;;;;;;;;;;;;UAmCA;;;;;;UAqCN;UAUA;;;;;;;;;;;UCpPO;;EAEf;;EAEA;;;;;;;;;;;;;;UAee;;;;;EAKf,IAAI,aAAa,cAAc,QAAQ;;;;;;EAOvC,IAAI,aAAa,aAAa,OAAO,GAAG,qBAAqB;;;;EAK7D,OAAO,cAAc;;;;;;;;;;;;;KCpBX;EACN;EAAa,eAAe;;EAC5B;;EACA;IAAS,UAAU,YAAY;;;qBAOxB;mBAEyC;mBACE;mBACV;mBAEzB;EALnB,YACoD,KAAK,YACH,OAAO,cACjB,WAAW,WAEpC,UAAU;EAG7B,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCIC,6BAA6B,cAAc;;;;;;SAM/C,QAAQ,SAAS,2BAA2B;;;;;SAa5C,aAAa,SAAS,mBAAmB,4BAA4B;;;;;;;;;EAqB5E,eAAe,aAAa;;;;;;;EAW5B,YAAY,SAAS;;;;qBCjGV;WACX;WACA;WACA;WACA;;;;;WAKA;;KAGU,2BAA2B,kCAAkC;;;;;;;;;;;;wBCSzD,yBAAyB,eAAe,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBCUpD,UAAU,eAAe,iBAAiB;;;;;;;;wBAmB1C,cAAc,gBAAgB;;;;;;;;;;;;;;;qBCnCjC,8BAA8B;mBACZ;EAA7B,YAA6B,OAAO;EAE9B,IAAI,GAAG,cAAc,QAAQ;EAI7B,IAAI,GAAG,aAAa,OAAO,GAAG,qBAAqB;EAOnD,OAAO,cAAc;;;;;;;;;;;;;qBCbhB,oCAAoC;mBAC9B;EAEjB,IAAI,GAAG,cAAc,QAAQ;EAU7B,IAAI,GAAG,aAAa,OAAO,GAAG,qBAAqB;EAKnD,OAAO,cAAc"}
|
|
@@ -1,84 +1,11 @@
|
|
|
1
|
-
import { l as Singleton, n as CONTAINER_TOKEN, p as inject, r as DI_TOKENS } from "../di-
|
|
2
|
-
import { n as __decorateParam, t as __decorate } from "../decorate-
|
|
1
|
+
import { l as Singleton, n as CONTAINER_TOKEN, p as inject, r as DI_TOKENS } from "../di-DQ3xbjgf.mjs";
|
|
2
|
+
import { n as __decorateParam, t as __decorate } from "../decorate-C79yt3Wt.mjs";
|
|
3
3
|
import { t as Macroable } from "../macroable-cvDTFZ_A.mjs";
|
|
4
|
-
import { n as Module } from "../module.decorator-
|
|
5
|
-
import { c as RATE_LIMITER_TOKENS, l as RateLimiterError, s as createThrottleMiddleware, u as TooManyRequestsError } from "../module-registry-
|
|
6
|
-
import "../module/index.mjs";
|
|
4
|
+
import { n as Module } from "../module.decorator-CYbrOlX9.mjs";
|
|
5
|
+
import { c as RATE_LIMITER_TOKENS, l as RateLimiterError, s as createThrottleMiddleware, u as TooManyRequestsError } from "../module-registry-lkQPDuQe.mjs";
|
|
7
6
|
import { CACHE_TOKENS, CacheModule } from "../cache/index.mjs";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A single rate limit window.
|
|
12
|
-
*
|
|
13
|
-
* Build via the static factories (`perSecond`, `perMinute`, `perMinutes`,
|
|
14
|
-
* `perHour`, `perDay`, `none`). Chain `.by(key)` to scope per-actor and
|
|
15
|
-
* `.response(handler)` to override the default 429 response.
|
|
16
|
-
*
|
|
17
|
-
* Returned (singly or as an array) by limiter resolvers registered via
|
|
18
|
-
* `RateLimiterRegistry.for()`.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```typescript
|
|
22
|
-
* Limit.perMinute(60).by(ctx.header('cf-connecting-ip') ?? 'global')
|
|
23
|
-
* Limit.perHour(100).by(userId)
|
|
24
|
-
* Limit.none() // bypass
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
var Limit = class Limit {
|
|
28
|
-
windowSeconds;
|
|
29
|
-
max;
|
|
30
|
-
disabled;
|
|
31
|
-
_key;
|
|
32
|
-
_customResponse;
|
|
33
|
-
constructor(windowSeconds, max, disabled = false) {
|
|
34
|
-
this.windowSeconds = windowSeconds;
|
|
35
|
-
this.max = max;
|
|
36
|
-
this.disabled = disabled;
|
|
37
|
-
}
|
|
38
|
-
static perSecond(max) {
|
|
39
|
-
return new Limit(1, max);
|
|
40
|
-
}
|
|
41
|
-
static perSeconds(seconds, max) {
|
|
42
|
-
return new Limit(seconds, max);
|
|
43
|
-
}
|
|
44
|
-
static perMinute(max) {
|
|
45
|
-
return new Limit(60, max);
|
|
46
|
-
}
|
|
47
|
-
static perMinutes(minutes, max) {
|
|
48
|
-
return new Limit(minutes * 60, max);
|
|
49
|
-
}
|
|
50
|
-
static perHour(max) {
|
|
51
|
-
return new Limit(3600, max);
|
|
52
|
-
}
|
|
53
|
-
static perDay(max) {
|
|
54
|
-
return new Limit(1440 * 60, max);
|
|
55
|
-
}
|
|
56
|
-
/** Bypass the limiter entirely for this request. */
|
|
57
|
-
static none() {
|
|
58
|
-
return new Limit(0, 0, true);
|
|
59
|
-
}
|
|
60
|
-
/** Scope this limit to a specific actor (user id, IP, tenant, etc.). */
|
|
61
|
-
by(key) {
|
|
62
|
-
this._key = String(key);
|
|
63
|
-
return this;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Override the default 429 response. The handler receives the standard
|
|
67
|
-
* `RateLimitHeaders` so it can spread them onto its own Response or omit
|
|
68
|
-
* them as it sees fit.
|
|
69
|
-
*/
|
|
70
|
-
response(handler) {
|
|
71
|
-
this._customResponse = handler;
|
|
72
|
-
return this;
|
|
73
|
-
}
|
|
74
|
-
get key() {
|
|
75
|
-
return this._key;
|
|
76
|
-
}
|
|
77
|
-
get customResponse() {
|
|
78
|
-
return this._customResponse;
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
//#endregion
|
|
7
|
+
import { t as Limit } from "../limit-BE0HNAF6.mjs";
|
|
8
|
+
import { n as getRateLimits, t as RateLimit } from "../rate-limit.decorator-PYQ9EYqs.mjs";
|
|
82
9
|
//#region src/rate-limiter/rate-limiter-registry.ts
|
|
83
10
|
let RateLimiterRegistry = class RateLimiterRegistry extends Macroable {
|
|
84
11
|
container;
|
|
@@ -124,8 +51,8 @@ let RateLimiterRegistry = class RateLimiterRegistry extends Macroable {
|
|
|
124
51
|
let mostRestrictive;
|
|
125
52
|
let exceeded;
|
|
126
53
|
for (const limit of active) {
|
|
127
|
-
const key = this.makeKey(name, limit.windowSeconds, limit.key);
|
|
128
|
-
const hit = await this.hit(key, limit.windowSeconds);
|
|
54
|
+
const key = this.makeKey(name, limit.windowSeconds, limit.key, limit.distinctValue !== void 0);
|
|
55
|
+
const hit = limit.distinctValue === void 0 ? await this.hit(key, limit.windowSeconds) : await this.hitDistinct(key, limit.windowSeconds, limit.distinctValue, limit.max);
|
|
129
56
|
if (hit.count > limit.max) {
|
|
130
57
|
if (!exceeded || hit.resetAt > exceeded.resetAt) exceeded = {
|
|
131
58
|
limit,
|
|
@@ -178,8 +105,65 @@ let RateLimiterRegistry = class RateLimiterRegistry extends Macroable {
|
|
|
178
105
|
await this.store.set(key, next, ttlSeconds);
|
|
179
106
|
return next;
|
|
180
107
|
}
|
|
181
|
-
|
|
182
|
-
|
|
108
|
+
/**
|
|
109
|
+
* A window that counts DISTINCT values rather than requests.
|
|
110
|
+
*
|
|
111
|
+
* Uses the same typed-KV store as {@link hit} — the store persists arbitrary values, so a
|
|
112
|
+
* cardinality window needs no store-interface change — but a key namespace of its own, since
|
|
113
|
+
* the two shapes are not interchangeable and a shared key would have each read the other's.
|
|
114
|
+
*
|
|
115
|
+
* A value already present is admitted WITHOUT a write, which both keeps the count stable and
|
|
116
|
+
* leaves the TTL alone: re-touching something already counted must not slide the window. The
|
|
117
|
+
* stored set therefore never exceeds `max`.
|
|
118
|
+
*
|
|
119
|
+
* The cap is a SOFT ceiling under an eventually-consistent store. This is a read-modify-write
|
|
120
|
+
* of the whole array, so N concurrent requests carrying N different values all read the same
|
|
121
|
+
* set and last-write-wins keeps one of them: a burst can overshoot `max`, and unlike a lost
|
|
122
|
+
* counter increment (which the next request re-adds) a lost set member is gone for the rest of
|
|
123
|
+
* the window. Intended for small caps — tens, not thousands; nothing bounds `max` itself, and
|
|
124
|
+
* the whole set is read and rewritten on every new value. A hard cap needs a
|
|
125
|
+
* strongly-consistent store, i.e. a Durable Object.
|
|
126
|
+
*/
|
|
127
|
+
async hitDistinct(key, windowSeconds, value, max) {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const existing = await this.store.get(key);
|
|
130
|
+
if (existing && existing.resetAt > now) {
|
|
131
|
+
if (existing.values.includes(value)) return {
|
|
132
|
+
count: existing.values.length,
|
|
133
|
+
resetAt: existing.resetAt
|
|
134
|
+
};
|
|
135
|
+
if (existing.values.length >= max) return {
|
|
136
|
+
count: existing.values.length + 1,
|
|
137
|
+
resetAt: existing.resetAt
|
|
138
|
+
};
|
|
139
|
+
const values = [...existing.values, value];
|
|
140
|
+
const ttlSeconds = Math.max(1, Math.ceil((existing.resetAt - now) / 1e3));
|
|
141
|
+
await this.store.set(key, {
|
|
142
|
+
values,
|
|
143
|
+
resetAt: existing.resetAt
|
|
144
|
+
}, ttlSeconds);
|
|
145
|
+
return {
|
|
146
|
+
count: values.length,
|
|
147
|
+
resetAt: existing.resetAt
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const resetAt = now + windowSeconds * 1e3;
|
|
151
|
+
await this.store.set(key, {
|
|
152
|
+
values: [value],
|
|
153
|
+
resetAt
|
|
154
|
+
}, Math.max(1, windowSeconds));
|
|
155
|
+
return {
|
|
156
|
+
count: 1,
|
|
157
|
+
resetAt
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Distinct windows carry a `d:` segment: they store `StoredDistinct` where a plain window
|
|
162
|
+
* stores `StoredHit`, and one limiter name can legitimately declare both on the same window
|
|
163
|
+
* and actor. Sharing a key would have each branch read the other's shape and throw.
|
|
164
|
+
*/
|
|
165
|
+
makeKey(name, windowSeconds, by, distinct) {
|
|
166
|
+
return `rl:${name}:${distinct ? "d:" : ""}${windowSeconds}:${by ?? "*"}`;
|
|
183
167
|
}
|
|
184
168
|
makeHeaders(limit, remaining, resetAt) {
|
|
185
169
|
const retryAfter = Math.max(1, Math.ceil((resetAt - Date.now()) / 1e3));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/rate-limiter/limit.ts","../../src/rate-limiter/rate-limiter-registry.ts","../../src/rate-limiter/stores/kv-store.ts","../../src/rate-limiter/stores/memory-store.ts","../../src/rate-limiter/stores/store-factory.ts","../../src/rate-limiter/rate-limiter.module.ts"],"sourcesContent":["import type { RouterContext } from '../router/router-context'\n\n/**\n * Standard rate-limit response headers passed to custom response handlers.\n * Keys are canonical HTTP header names; values are the stringified counts.\n */\nexport interface RateLimitHeaders {\n 'Retry-After': string\n 'X-RateLimit-Limit': string\n 'X-RateLimit-Remaining': string\n 'X-RateLimit-Reset': string\n}\n\n/**\n * Custom response handler invoked when a limit is exceeded and the user\n * supplied `.response()` on the {@link Limit}. Receives the request context\n * and the precomputed standard headers (which the handler can spread, drop,\n * or override).\n */\nexport type RateLimitResponseHandler = (\n ctx: RouterContext,\n headers: RateLimitHeaders,\n) => Response | Promise<Response>\n\n/**\n * A single rate limit window.\n *\n * Build via the static factories (`perSecond`, `perMinute`, `perMinutes`,\n * `perHour`, `perDay`, `none`). Chain `.by(key)` to scope per-actor and\n * `.response(handler)` to override the default 429 response.\n *\n * Returned (singly or as an array) by limiter resolvers registered via\n * `RateLimiterRegistry.for()`.\n *\n * @example\n * ```typescript\n * Limit.perMinute(60).by(ctx.header('cf-connecting-ip') ?? 'global')\n * Limit.perHour(100).by(userId)\n * Limit.none() // bypass\n * ```\n */\nexport class Limit {\n private _key?: string\n private _customResponse?: RateLimitResponseHandler\n\n private constructor(\n public readonly windowSeconds: number,\n public readonly max: number,\n public readonly disabled = false,\n ) {}\n\n static perSecond(max: number): Limit {\n return new Limit(1, max)\n }\n\n static perSeconds(seconds: number, max: number): Limit {\n return new Limit(seconds, max)\n }\n\n static perMinute(max: number): Limit {\n return new Limit(60, max)\n }\n\n static perMinutes(minutes: number, max: number): Limit {\n return new Limit(minutes * 60, max)\n }\n\n static perHour(max: number): Limit {\n return new Limit(60 * 60, max)\n }\n\n static perDay(max: number): Limit {\n return new Limit(24 * 60 * 60, max)\n }\n\n /** Bypass the limiter entirely for this request. */\n static none(): Limit {\n return new Limit(0, 0, true)\n }\n\n /** Scope this limit to a specific actor (user id, IP, tenant, etc.). */\n by(key: string | number): this {\n this._key = String(key)\n return this\n }\n\n /**\n * Override the default 429 response. The handler receives the standard\n * `RateLimitHeaders` so it can spread them onto its own Response or omit\n * them as it sees fit.\n */\n response(handler: RateLimitResponseHandler): this {\n this._customResponse = handler\n return this\n }\n\n get key(): string | undefined {\n return this._key\n }\n\n get customResponse(): RateLimitResponseHandler | undefined {\n return this._customResponse\n }\n}\n","import { type Container, DI_TOKENS, inject } from '../di'\nimport { Singleton } from '../di/decorators'\nimport { Macroable } from '../macroable'\nimport type { Next } from '../router/middleware.interface'\nimport type { RouterContext } from '../router/router-context'\nimport { RateLimiterError, TooManyRequestsError } from './errors'\nimport type { Limit, RateLimitHeaders } from './limit'\nimport { RATE_LIMITER_TOKENS } from './rate-limiter.tokens'\nimport type { IRateLimiterStore, RateLimitHit } from './stores/rate-limiter-store.interface'\n\n/**\n * Resolver function registered via {@link RateLimiterRegistry.for}. Receives\n * the request context and returns the limit (or limits) that apply to this\n * request. Return `Limit.none()` to bypass for the current request.\n */\nexport type LimitResolver = (\n ctx: RouterContext,\n) => Limit | Limit[] | Promise<Limit | Limit[]>\n\ninterface StoredHit {\n count: number\n resetAt: number\n}\n\n/**\n * Central registry of named rate limiters and the request-time enforcement\n * pipeline. Resolved as a singleton; consumed by `ThrottleMiddleware`.\n *\n * Register limiters in a module's `onInitialize` hook:\n * ```typescript\n * @Module({})\n * export class RateLimitsModule implements OnInitialize {\n * onInitialize({ container }: ModuleContext): void {\n * const limiter = container.resolve<RateLimiterRegistry>(RATE_LIMITER_TOKENS.Registry)\n * limiter.for('api', (ctx) => Limit.perMinute(60).by(ctx.header('cf-connecting-ip') ?? '*'))\n * }\n * }\n * ```\n *\n * Extensible via `Macroable`: adapter packages (e.g. `@stratal/framework/auth`)\n * can attach extra registration methods such as `forPath()` for better-auth\n * `customRules` interop.\n */\n// IMPORTANT: do not pass a token to @Singleton — that would self-register\n// the class globally at module-load time, making the Registry resolvable\n// even when the user never imported RateLimiterModule. We rely on\n// RateLimiterModule providers being the only binding source, so\n// `{ isOptional: true }` in ThrottleMiddleware correctly returns undefined\n// when the module is missing.\n@Singleton()\nexport class RateLimiterRegistry extends Macroable {\n private readonly resolvers = new Map<string, LimitResolver>()\n\n constructor(\n @inject(DI_TOKENS.Container) private readonly container: Container,\n ) {\n super()\n }\n\n /**\n * Resolve the store per access instead of capturing it at construction.\n * The registry is a singleton built during module init; lazy resolution\n * keeps later `RATE_LIMITER_TOKENS.Store` overrides (e.g. the testing\n * harness disabling limits) effective.\n */\n private get store(): IRateLimiterStore {\n return this.container.resolve<IRateLimiterStore>(RATE_LIMITER_TOKENS.Store)\n }\n\n /**\n * Register a named limiter. Names must be unique; calling `for()` again\n * with the same name overwrites the previous resolver (matches Laravel\n * `RateLimiter::for` semantics — last definition wins).\n */\n for(name: string, resolver: LimitResolver): void {\n this.resolvers.set(name, resolver)\n }\n\n has(name: string): boolean {\n return this.resolvers.has(name)\n }\n\n /**\n * Enforce the named limiter for the current request. Called by\n * `ThrottleMiddleware` (the per-name class produced by\n * `createThrottleMiddleware`). Resolves the limiter, increments the store\n * for each non-bypassed limit, sets `X-RateLimit-*` headers on success, and\n * either invokes the limit's custom `.response()` or throws\n * {@link TooManyRequestsError} when a limit is exceeded.\n */\n async handle(name: string, ctx: RouterContext, next: Next): Promise<Response | void> {\n const resolver = this.resolvers.get(name)\n if (!resolver) {\n throw new RateLimiterError(`Rate limiter \"${name}\" is not defined. Register it with limiter.for(\"${name}\", ...) in a module's onInitialize hook.`)\n }\n\n const resolved = await resolver(ctx)\n const limits = Array.isArray(resolved) ? resolved : [resolved]\n const active = limits.filter((l) => !l.disabled)\n\n if (active.length === 0) {\n return next()\n }\n\n let mostRestrictive: { limit: Limit; remaining: number; resetAt: number } | undefined\n let exceeded: { limit: Limit; resetAt: number } | undefined\n\n for (const limit of active) {\n const key = this.makeKey(name, limit.windowSeconds, limit.key)\n const hit = await this.hit(key, limit.windowSeconds)\n\n if (hit.count > limit.max) {\n if (!exceeded || hit.resetAt > exceeded.resetAt) {\n exceeded = { limit, resetAt: hit.resetAt }\n }\n continue\n }\n\n const remaining = limit.max - hit.count\n if (!mostRestrictive || remaining < mostRestrictive.remaining) {\n mostRestrictive = { limit, remaining, resetAt: hit.resetAt }\n }\n }\n\n if (exceeded) {\n const headers = this.makeHeaders(exceeded.limit.max, 0, exceeded.resetAt)\n if (exceeded.limit.customResponse) {\n return exceeded.limit.customResponse(ctx, headers)\n }\n throw new TooManyRequestsError({\n retryAfter: Number(headers['Retry-After']),\n limit: exceeded.limit.max,\n resetAt: exceeded.resetAt,\n })\n }\n\n await next()\n\n if (mostRestrictive) {\n const headers = this.makeHeaders(\n mostRestrictive.limit.max,\n mostRestrictive.remaining,\n mostRestrictive.resetAt,\n )\n // Hono populates ctx.c.res after next() — same pattern as logger.middleware.ts.\n const downstream = ctx.c.res\n downstream.headers.set('X-RateLimit-Limit', headers['X-RateLimit-Limit'])\n downstream.headers.set('X-RateLimit-Remaining', headers['X-RateLimit-Remaining'])\n downstream.headers.set('X-RateLimit-Reset', headers['X-RateLimit-Reset'])\n }\n }\n\n /**\n * Get-modify-set increment over the typed KV store. Not atomic across\n * concurrent edge requests on KV — see `KvRateLimiterStore`'s caveat.\n */\n private async hit(key: string, windowSeconds: number): Promise<RateLimitHit> {\n const now = Date.now()\n const existing = await this.store.get<StoredHit>(key)\n\n let next: StoredHit\n if (!existing || existing.resetAt <= now) {\n next = { count: 1, resetAt: now + windowSeconds * 1000 }\n } else {\n next = { count: existing.count + 1, resetAt: existing.resetAt }\n }\n\n const ttlSeconds = Math.max(1, Math.ceil((next.resetAt - now) / 1000))\n await this.store.set(key, next, ttlSeconds)\n return next\n }\n\n private makeKey(name: string, windowSeconds: number, by: string | undefined): string {\n const actor = by ?? '*'\n return `rl:${name}:${windowSeconds}:${actor}`\n }\n\n private makeHeaders(limit: number, remaining: number, resetAt: number): RateLimitHeaders {\n const retryAfter = Math.max(1, Math.ceil((resetAt - Date.now()) / 1000))\n return {\n 'Retry-After': String(retryAfter),\n 'X-RateLimit-Limit': String(limit),\n 'X-RateLimit-Remaining': String(Math.max(0, remaining)),\n 'X-RateLimit-Reset': String(Math.ceil(resetAt / 1000)),\n }\n }\n}\n","import type { CacheService } from '../../cache/services/cache.service'\nimport type { IRateLimiterStore } from './rate-limiter-store.interface'\n\n/**\n * Cloudflare KV-backed typed KV store.\n *\n * KV's minimum `expirationTtl` is 60 seconds; sub-60s windows are still\n * enforced by the registry's algorithm via the persisted `resetAt`, but\n * the key itself may live in KV longer than the logical window.\n *\n * KV has no native atomic increment, so concurrent writes from different\n * edge locations may undercount under high contention. That's an inherent\n * KV tradeoff — pick `{ useClass: MyDurableObjectStore }` for strict\n * accuracy across edges.\n */\nexport class KvRateLimiterStore implements IRateLimiterStore {\n constructor(private readonly cache: CacheService) {}\n\n async get<T>(key: string): Promise<T | null> {\n return (await this.cache.get<T>(key, 'json')) ?? null\n }\n\n async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {\n const ttl = Math.max(60, Math.ceil(ttlSeconds))\n await this.cache.put(key, JSON.stringify(value), { expirationTtl: ttl })\n }\n\n async delete(key: string): Promise<void> {\n await this.cache.delete(key)\n }\n}\n","import type { IRateLimiterStore } from './rate-limiter-store.interface'\n\ninterface Entry {\n value: unknown\n expiresAt: number\n}\n\n/**\n * In-process Map-backed typed KV store.\n *\n * Suitable for tests and single-isolate scenarios. Not safe across\n * Cloudflare Worker isolates — entries reset whenever a fresh isolate\n * spins up. Use `KvRateLimiterStore` (or a custom Durable Object store)\n * for production. Expiry is lazy: stale entries are dropped on the next\n * `get`.\n */\nexport class InMemoryRateLimiterStore implements IRateLimiterStore {\n private readonly entries = new Map<string, Entry>()\n\n get<T>(key: string): Promise<T | null> {\n const entry = this.entries.get(key)\n if (!entry) return Promise.resolve(null)\n if (entry.expiresAt <= Date.now()) {\n this.entries.delete(key)\n return Promise.resolve(null)\n }\n return Promise.resolve(entry.value as T)\n }\n\n set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {\n this.entries.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 })\n return Promise.resolve()\n }\n\n delete(key: string): Promise<void> {\n this.entries.delete(key)\n return Promise.resolve()\n }\n}\n","import { inject } from '../../di'\nimport { CACHE_TOKENS } from '../../cache/cache.tokens'\nimport type { CacheService } from '../../cache/services/cache.service'\nimport { CONTAINER_TOKEN, type Container } from '../../di'\nimport { Singleton } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport type { StratalEnv } from '../../env'\nimport type { Constructor } from '../../types'\nimport { RateLimiterError } from '../errors'\nimport { RATE_LIMITER_TOKENS } from '../rate-limiter.tokens'\nimport { KvRateLimiterStore } from './kv-store'\nimport { InMemoryRateLimiterStore } from './memory-store'\nimport type { IRateLimiterStore } from './rate-limiter-store.interface'\n\n/**\n * Configuration for `RateLimiterModule.forRoot()`. Picks the backing store.\n *\n * - `'kv'`: Cloudflare KV. `binding` names the KVNamespace on `StratalEnv`.\n * - `'memory'`: in-process Map. Tests / single-isolate only.\n * - `{ useClass }`: any class implementing `IRateLimiterStore`. Resolved\n * from the DI container (so the class can declare its own `@inject` deps,\n * e.g. a Durable Object namespace from `StratalEnv`).\n */\nexport type RateLimiterModuleOptions =\n | { store: 'kv'; binding: keyof StratalEnv }\n | { store: 'memory' }\n | { store: { useClass: Constructor<IRateLimiterStore> } }\n\n// IMPORTANT: see RateLimiterRegistry — no token on @Singleton so the\n// factory isn't globally bound at class-load time. Module providers are\n// the sole binding source, which keeps the \"module not imported\" detection\n// in ThrottleMiddleware working.\n@Singleton()\nexport class RateLimiterStoreFactory {\n constructor(\n @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv,\n @inject(CACHE_TOKENS.CacheService) private readonly cache: CacheService,\n @inject(CONTAINER_TOKEN) private readonly container: Container,\n @inject(RATE_LIMITER_TOKENS.Options, { isOptional: true })\n private readonly options?: RateLimiterModuleOptions,\n ) {}\n\n create(): IRateLimiterStore {\n if (!this.options) {\n throw new RateLimiterError('RateLimiterModule is not configured. Call RateLimiterModule.forRoot({ store: ... }) to configure a backing store.')\n }\n\n const { store } = this.options\n\n if (store === 'memory') {\n return new InMemoryRateLimiterStore()\n }\n\n if (store === 'kv') {\n const binding = this.env[this.options.binding] as KVNamespace | undefined\n if (!binding) {\n throw new RateLimiterError(`KV binding \"${String(this.options.binding)}\" is not available in the environment.`)\n }\n return new KvRateLimiterStore(this.cache.withBinding(binding))\n }\n\n return this.container.resolve<IRateLimiterStore>(store.useClass)\n }\n}\n","import { CacheModule } from '../cache'\nimport type { ExceptionHandler } from '../errors/exception-handler'\nimport { Module } from '../module'\nimport type { AsyncModuleOptions, DynamicModule, ModuleContext, OnException, OnInitialize } from '../module/types'\nimport { TooManyRequestsError } from './errors'\nimport { RateLimiterRegistry } from './rate-limiter-registry'\nimport { RATE_LIMITER_TOKENS } from './rate-limiter.tokens'\nimport { RateLimiterStoreFactory, type RateLimiterModuleOptions } from './stores/store-factory'\nimport type { IRateLimiterStore } from './stores/rate-limiter-store.interface'\n\n/**\n * Rate limiter module — opt-in, NOT registered automatically by Application.\n *\n * Usage:\n * ```typescript\n * @Module({\n * imports: [RateLimiterModule.forRoot({ store: 'kv', binding: 'RATE_LIMITS' })],\n * })\n * export class AppModule {}\n * ```\n *\n * Define limiters in any module's `onInitialize` hook by resolving\n * `RATE_LIMITER_TOKENS.Registry` from the container. Apply them with\n * `router.throttle('name')` or `@RateLimit('name')`.\n *\n * The module:\n * - eagerly validates the store at app boot (`onInitialize` resolves the\n * factory and calls `create()`); a missing `forRoot` surfaces\n * `RateLimiterError` before any request is served.\n * - registers a `respond()` callback on the `ExceptionHandler` (via\n * `onException`) that injects `Retry-After` and `X-RateLimit-*` headers\n * on every {@link TooManyRequestsError} response, regardless of whether\n * the body was rendered as JSON, HTML, or via Inertia.\n */\n@Module({\n imports: [CacheModule],\n providers: [\n { provide: RATE_LIMITER_TOKENS.Registry, useClass: RateLimiterRegistry },\n { provide: RATE_LIMITER_TOKENS.StoreFactory, useClass: RateLimiterStoreFactory },\n {\n provide: RATE_LIMITER_TOKENS.Store,\n useFactory: (factory: RateLimiterStoreFactory) => factory.create(),\n inject: [RATE_LIMITER_TOKENS.StoreFactory],\n },\n ],\n})\nexport class RateLimiterModule implements OnInitialize, OnException {\n /**\n * Configure the rate limiter with a store choice.\n *\n * @param options - `{ store: 'kv', binding }` | `{ store: 'memory' }` | `{ store: { useClass } }`\n */\n static forRoot(options: RateLimiterModuleOptions): DynamicModule {\n return {\n module: RateLimiterModule,\n providers: [\n { provide: RATE_LIMITER_TOKENS.Options, useValue: options },\n ],\n }\n }\n\n /**\n * Async configuration. Use when store options depend on other services\n * (e.g. ConfigService).\n */\n static forRootAsync(options: AsyncModuleOptions<RateLimiterModuleOptions>): DynamicModule {\n return {\n module: RateLimiterModule,\n providers: [\n {\n provide: RATE_LIMITER_TOKENS.Options,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n\n /**\n * Eagerly resolve the store so a missing `forRoot()` fails at boot,\n * not on the first throttled request. Also registers a per-app marker\n * value so ThrottleMiddleware can distinguish \"module imported into this\n * app\" from \"module class loaded somewhere in the process\" (the latter is\n * indistinguishable at the DI layer because @Module's `registry()` call\n * binds providers globally at decoration time).\n */\n onInitialize({ container }: ModuleContext): void {\n container.resolve<IRateLimiterStore>(RATE_LIMITER_TOKENS.Store)\n container.registerValue(RATE_LIMITER_TOKENS.ModuleMarker, { imported: true })\n }\n\n /**\n * Inject `Retry-After` + `X-RateLimit-*` headers on every 429 response.\n * The body itself is rendered by `ExceptionHandler.defaultRender` —\n * content-negotiated, so HTML clients (browsers, Inertia) and JSON\n * clients both get the right shape.\n */\n onException(handler: ExceptionHandler): void {\n handler.dontReport([TooManyRequestsError])\n handler.respond((response, error) => {\n if (!(error instanceof TooManyRequestsError)) return response\n response.headers.set('Retry-After', String(error.info.retryAfter))\n response.headers.set('X-RateLimit-Limit', String(error.info.limit))\n response.headers.set('X-RateLimit-Remaining', '0')\n response.headers.set('X-RateLimit-Reset', String(Math.ceil(error.info.resetAt / 1000)))\n return response\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/rate-limiter/rate-limiter-registry.ts","../../src/rate-limiter/stores/kv-store.ts","../../src/rate-limiter/stores/memory-store.ts","../../src/rate-limiter/stores/store-factory.ts","../../src/rate-limiter/rate-limiter.module.ts"],"sourcesContent":["import { type Container, DI_TOKENS, inject } from '../di'\nimport { Singleton } from '../di/decorators'\nimport { Macroable } from '../macroable'\nimport type { Next } from '../router/middleware.interface'\nimport type { RouterContext } from '../router/router-context'\nimport { RateLimiterError, TooManyRequestsError } from './errors'\nimport type { Limit, RateLimitHeaders } from './limit'\nimport { RATE_LIMITER_TOKENS } from './rate-limiter.tokens'\nimport type { IRateLimiterStore, RateLimitHit } from './stores/rate-limiter-store.interface'\n\n/**\n * Resolver function registered via {@link RateLimiterRegistry.for}. Receives\n * the request context and returns the limit (or limits) that apply to this\n * request. Return `Limit.none()` to bypass for the current request.\n */\nexport type LimitResolver = (\n ctx: RouterContext,\n) => Limit | Limit[] | Promise<Limit | Limit[]>\n\ninterface StoredHit {\n count: number\n resetAt: number\n}\n\n/** What a distinct-value window stores, in place of `StoredHit`'s counter. */\ninterface StoredDistinct {\n values: string[]\n resetAt: number\n}\n\n/**\n * Central registry of named rate limiters and the request-time enforcement\n * pipeline. Resolved as a singleton; consumed by `ThrottleMiddleware`.\n *\n * Register limiters in a module's `onInitialize` hook:\n * ```typescript\n * @Module({})\n * export class RateLimitsModule implements OnInitialize {\n * onInitialize({ container }: ModuleContext): void {\n * const limiter = container.resolve<RateLimiterRegistry>(RATE_LIMITER_TOKENS.Registry)\n * limiter.for('api', (ctx) => Limit.perMinute(60).by(ctx.header('cf-connecting-ip') ?? '*'))\n * }\n * }\n * ```\n *\n * Extensible via `Macroable`: adapter packages (e.g. `@stratal/framework/auth`)\n * can attach extra registration methods such as `forPath()` for better-auth\n * `customRules` interop.\n */\n// IMPORTANT: do not pass a token to @Singleton — that would self-register\n// the class globally at module-load time, making the Registry resolvable\n// even when the user never imported RateLimiterModule. We rely on\n// RateLimiterModule providers being the only binding source, so\n// `{ isOptional: true }` in ThrottleMiddleware correctly returns undefined\n// when the module is missing.\n@Singleton()\nexport class RateLimiterRegistry extends Macroable {\n private readonly resolvers = new Map<string, LimitResolver>()\n\n constructor(\n @inject(DI_TOKENS.Container) private readonly container: Container,\n ) {\n super()\n }\n\n /**\n * Resolve the store per access instead of capturing it at construction.\n * The registry is a singleton built during module init; lazy resolution\n * keeps later `RATE_LIMITER_TOKENS.Store` overrides (e.g. the testing\n * harness disabling limits) effective.\n */\n private get store(): IRateLimiterStore {\n return this.container.resolve<IRateLimiterStore>(RATE_LIMITER_TOKENS.Store)\n }\n\n /**\n * Register a named limiter. Names must be unique; calling `for()` again\n * with the same name overwrites the previous resolver (matches Laravel\n * `RateLimiter::for` semantics — last definition wins).\n */\n for(name: string, resolver: LimitResolver): void {\n this.resolvers.set(name, resolver)\n }\n\n has(name: string): boolean {\n return this.resolvers.has(name)\n }\n\n /**\n * Enforce the named limiter for the current request. Called by\n * `ThrottleMiddleware` (the per-name class produced by\n * `createThrottleMiddleware`). Resolves the limiter, increments the store\n * for each non-bypassed limit, sets `X-RateLimit-*` headers on success, and\n * either invokes the limit's custom `.response()` or throws\n * {@link TooManyRequestsError} when a limit is exceeded.\n */\n async handle(name: string, ctx: RouterContext, next: Next): Promise<Response | void> {\n const resolver = this.resolvers.get(name)\n if (!resolver) {\n throw new RateLimiterError(`Rate limiter \"${name}\" is not defined. Register it with limiter.for(\"${name}\", ...) in a module's onInitialize hook.`)\n }\n\n const resolved = await resolver(ctx)\n const limits = Array.isArray(resolved) ? resolved : [resolved]\n const active = limits.filter((l) => !l.disabled)\n\n if (active.length === 0) {\n return next()\n }\n\n let mostRestrictive: { limit: Limit; remaining: number; resetAt: number } | undefined\n let exceeded: { limit: Limit; resetAt: number } | undefined\n\n for (const limit of active) {\n const key = this.makeKey(name, limit.windowSeconds, limit.key, limit.distinctValue !== undefined)\n const hit =\n limit.distinctValue === undefined\n ? await this.hit(key, limit.windowSeconds)\n : await this.hitDistinct(key, limit.windowSeconds, limit.distinctValue, limit.max)\n\n if (hit.count > limit.max) {\n if (!exceeded || hit.resetAt > exceeded.resetAt) {\n exceeded = { limit, resetAt: hit.resetAt }\n }\n continue\n }\n\n const remaining = limit.max - hit.count\n if (!mostRestrictive || remaining < mostRestrictive.remaining) {\n mostRestrictive = { limit, remaining, resetAt: hit.resetAt }\n }\n }\n\n if (exceeded) {\n const headers = this.makeHeaders(exceeded.limit.max, 0, exceeded.resetAt)\n if (exceeded.limit.customResponse) {\n return exceeded.limit.customResponse(ctx, headers)\n }\n throw new TooManyRequestsError({\n retryAfter: Number(headers['Retry-After']),\n limit: exceeded.limit.max,\n resetAt: exceeded.resetAt,\n })\n }\n\n await next()\n\n if (mostRestrictive) {\n const headers = this.makeHeaders(\n mostRestrictive.limit.max,\n mostRestrictive.remaining,\n mostRestrictive.resetAt,\n )\n // Hono populates ctx.c.res after next() — same pattern as logger.middleware.ts.\n //\n // Stamped unconditionally: the cache decision is not on the response yet at this point in\n // the unwind, so whether these may be kept is decided by\n // `createNoStoreFallbackMiddleware`, which strips them from a shared-cacheable response.\n const downstream = ctx.c.res\n downstream.headers.set('X-RateLimit-Limit', headers['X-RateLimit-Limit'])\n downstream.headers.set('X-RateLimit-Remaining', headers['X-RateLimit-Remaining'])\n downstream.headers.set('X-RateLimit-Reset', headers['X-RateLimit-Reset'])\n }\n }\n\n /**\n * Get-modify-set increment over the typed KV store. Not atomic across\n * concurrent edge requests on KV — see `KvRateLimiterStore`'s caveat.\n */\n private async hit(key: string, windowSeconds: number): Promise<RateLimitHit> {\n const now = Date.now()\n const existing = await this.store.get<StoredHit>(key)\n\n let next: StoredHit\n if (!existing || existing.resetAt <= now) {\n next = { count: 1, resetAt: now + windowSeconds * 1000 }\n } else {\n next = { count: existing.count + 1, resetAt: existing.resetAt }\n }\n\n const ttlSeconds = Math.max(1, Math.ceil((next.resetAt - now) / 1000))\n await this.store.set(key, next, ttlSeconds)\n return next\n }\n\n /**\n * A window that counts DISTINCT values rather than requests.\n *\n * Uses the same typed-KV store as {@link hit} — the store persists arbitrary values, so a\n * cardinality window needs no store-interface change — but a key namespace of its own, since\n * the two shapes are not interchangeable and a shared key would have each read the other's.\n *\n * A value already present is admitted WITHOUT a write, which both keeps the count stable and\n * leaves the TTL alone: re-touching something already counted must not slide the window. The\n * stored set therefore never exceeds `max`.\n *\n * The cap is a SOFT ceiling under an eventually-consistent store. This is a read-modify-write\n * of the whole array, so N concurrent requests carrying N different values all read the same\n * set and last-write-wins keeps one of them: a burst can overshoot `max`, and unlike a lost\n * counter increment (which the next request re-adds) a lost set member is gone for the rest of\n * the window. Intended for small caps — tens, not thousands; nothing bounds `max` itself, and\n * the whole set is read and rewritten on every new value. A hard cap needs a\n * strongly-consistent store, i.e. a Durable Object.\n */\n private async hitDistinct(\n key: string,\n windowSeconds: number,\n value: string,\n max: number,\n ): Promise<RateLimitHit> {\n const now = Date.now()\n const existing = await this.store.get<StoredDistinct>(key)\n\n if (existing && existing.resetAt > now) {\n if (existing.values.includes(value)) {\n return { count: existing.values.length, resetAt: existing.resetAt }\n }\n\n // A new value past the cap is refused WITHOUT being stored: persisting it would push the\n // stored count past `max` for every later request, including ones for values already\n // counted, which must stay admitted.\n if (existing.values.length >= max) {\n return { count: existing.values.length + 1, resetAt: existing.resetAt }\n }\n\n const values = [...existing.values, value]\n const ttlSeconds = Math.max(1, Math.ceil((existing.resetAt - now) / 1000))\n await this.store.set(key, { values, resetAt: existing.resetAt }, ttlSeconds)\n return { count: values.length, resetAt: existing.resetAt }\n }\n\n const resetAt = now + windowSeconds * 1000\n await this.store.set(key, { values: [value], resetAt }, Math.max(1, windowSeconds))\n return { count: 1, resetAt }\n }\n\n /**\n * Distinct windows carry a `d:` segment: they store `StoredDistinct` where a plain window\n * stores `StoredHit`, and one limiter name can legitimately declare both on the same window\n * and actor. Sharing a key would have each branch read the other's shape and throw.\n */\n private makeKey(\n name: string,\n windowSeconds: number,\n by: string | undefined,\n distinct: boolean,\n ): string {\n const actor = by ?? '*'\n return `rl:${name}:${distinct ? 'd:' : ''}${windowSeconds}:${actor}`\n }\n\n private makeHeaders(limit: number, remaining: number, resetAt: number): RateLimitHeaders {\n const retryAfter = Math.max(1, Math.ceil((resetAt - Date.now()) / 1000))\n return {\n 'Retry-After': String(retryAfter),\n 'X-RateLimit-Limit': String(limit),\n 'X-RateLimit-Remaining': String(Math.max(0, remaining)),\n 'X-RateLimit-Reset': String(Math.ceil(resetAt / 1000)),\n }\n }\n}\n","import type { CacheService } from '../../cache/services/cache.service'\nimport type { IRateLimiterStore } from './rate-limiter-store.interface'\n\n/**\n * Cloudflare KV-backed typed KV store.\n *\n * KV's minimum `expirationTtl` is 60 seconds; sub-60s windows are still\n * enforced by the registry's algorithm via the persisted `resetAt`, but\n * the key itself may live in KV longer than the logical window.\n *\n * KV has no native atomic increment, so concurrent writes from different\n * edge locations may undercount under high contention. That's an inherent\n * KV tradeoff — pick `{ useClass: MyDurableObjectStore }` for strict\n * accuracy across edges.\n */\nexport class KvRateLimiterStore implements IRateLimiterStore {\n constructor(private readonly cache: CacheService) {}\n\n async get<T>(key: string): Promise<T | null> {\n return (await this.cache.get<T>(key, 'json')) ?? null\n }\n\n async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {\n const ttl = Math.max(60, Math.ceil(ttlSeconds))\n // `CacheService.put` defers the KV write via `waitUntil`, so this no longer\n // blocks the request on KV's central-store write latency.\n await this.cache.put(key, JSON.stringify(value), { expirationTtl: ttl })\n }\n\n async delete(key: string): Promise<void> {\n await this.cache.delete(key)\n }\n}\n","import type { IRateLimiterStore } from './rate-limiter-store.interface'\n\ninterface Entry {\n value: unknown\n expiresAt: number\n}\n\n/**\n * In-process Map-backed typed KV store.\n *\n * Suitable for tests and single-isolate scenarios. Not safe across\n * Cloudflare Worker isolates — entries reset whenever a fresh isolate\n * spins up. Use `KvRateLimiterStore` (or a custom Durable Object store)\n * for production. Expiry is lazy: stale entries are dropped on the next\n * `get`.\n */\nexport class InMemoryRateLimiterStore implements IRateLimiterStore {\n private readonly entries = new Map<string, Entry>()\n\n get<T>(key: string): Promise<T | null> {\n const entry = this.entries.get(key)\n if (!entry) return Promise.resolve(null)\n if (entry.expiresAt <= Date.now()) {\n this.entries.delete(key)\n return Promise.resolve(null)\n }\n return Promise.resolve(entry.value as T)\n }\n\n set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {\n this.entries.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 })\n return Promise.resolve()\n }\n\n delete(key: string): Promise<void> {\n this.entries.delete(key)\n return Promise.resolve()\n }\n}\n","import { inject } from '../../di'\nimport { CACHE_TOKENS } from '../../cache/cache.tokens'\nimport type { CacheService } from '../../cache/services/cache.service'\nimport { CONTAINER_TOKEN, type Container } from '../../di'\nimport { Singleton } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport type { StratalEnv } from '../../env'\nimport type { Constructor } from '../../types'\nimport { RateLimiterError } from '../errors'\nimport { RATE_LIMITER_TOKENS } from '../rate-limiter.tokens'\nimport { KvRateLimiterStore } from './kv-store'\nimport { InMemoryRateLimiterStore } from './memory-store'\nimport type { IRateLimiterStore } from './rate-limiter-store.interface'\n\n/**\n * Configuration for `RateLimiterModule.forRoot()`. Picks the backing store.\n *\n * - `'kv'`: Cloudflare KV. `binding` names the KVNamespace on `StratalEnv`.\n * - `'memory'`: in-process Map. Tests / single-isolate only.\n * - `{ useClass }`: any class implementing `IRateLimiterStore`. Resolved\n * from the DI container (so the class can declare its own `@inject` deps,\n * e.g. a Durable Object namespace from `StratalEnv`).\n */\nexport type RateLimiterModuleOptions =\n | { store: 'kv'; binding: keyof StratalEnv }\n | { store: 'memory' }\n | { store: { useClass: Constructor<IRateLimiterStore> } }\n\n// IMPORTANT: see RateLimiterRegistry — no token on @Singleton so the\n// factory isn't globally bound at class-load time. Module providers are\n// the sole binding source, which keeps the \"module not imported\" detection\n// in ThrottleMiddleware working.\n@Singleton()\nexport class RateLimiterStoreFactory {\n constructor(\n @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv,\n @inject(CACHE_TOKENS.CacheService) private readonly cache: CacheService,\n @inject(CONTAINER_TOKEN) private readonly container: Container,\n @inject(RATE_LIMITER_TOKENS.Options, { isOptional: true })\n private readonly options?: RateLimiterModuleOptions,\n ) {}\n\n create(): IRateLimiterStore {\n if (!this.options) {\n throw new RateLimiterError('RateLimiterModule is not configured. Call RateLimiterModule.forRoot({ store: ... }) to configure a backing store.')\n }\n\n const { store } = this.options\n\n if (store === 'memory') {\n return new InMemoryRateLimiterStore()\n }\n\n if (store === 'kv') {\n const binding = this.env[this.options.binding] as KVNamespace | undefined\n if (!binding) {\n throw new RateLimiterError(`KV binding \"${String(this.options.binding)}\" is not available in the environment.`)\n }\n return new KvRateLimiterStore(this.cache.withBinding(binding))\n }\n\n return this.container.resolve<IRateLimiterStore>(store.useClass)\n }\n}\n","import { CacheModule } from '../cache'\nimport type { ExceptionHandler } from '../errors/exception-handler'\nimport { Module } from '../module'\nimport type { AsyncModuleOptions, DynamicModule, ModuleContext, OnException, OnInitialize } from '../module/types'\nimport { TooManyRequestsError } from './errors'\nimport { RateLimiterRegistry } from './rate-limiter-registry'\nimport { RATE_LIMITER_TOKENS } from './rate-limiter.tokens'\nimport { RateLimiterStoreFactory, type RateLimiterModuleOptions } from './stores/store-factory'\nimport type { IRateLimiterStore } from './stores/rate-limiter-store.interface'\n\n/**\n * Rate limiter module — opt-in, NOT registered automatically by Application.\n *\n * Usage:\n * ```typescript\n * @Module({\n * imports: [RateLimiterModule.forRoot({ store: 'kv', binding: 'RATE_LIMITS' })],\n * })\n * export class AppModule {}\n * ```\n *\n * Define limiters in any module's `onInitialize` hook by resolving\n * `RATE_LIMITER_TOKENS.Registry` from the container. Apply them with\n * `router.throttle('name')` or `@RateLimit('name')`.\n *\n * The module:\n * - eagerly validates the store at app boot (`onInitialize` resolves the\n * factory and calls `create()`); a missing `forRoot` surfaces\n * `RateLimiterError` before any request is served.\n * - registers a `respond()` callback on the `ExceptionHandler` (via\n * `onException`) that injects `Retry-After` and `X-RateLimit-*` headers\n * on every {@link TooManyRequestsError} response, regardless of whether\n * the body was rendered as JSON, HTML, or via Inertia.\n */\n@Module({\n imports: [CacheModule],\n providers: [\n { provide: RATE_LIMITER_TOKENS.Registry, useClass: RateLimiterRegistry },\n { provide: RATE_LIMITER_TOKENS.StoreFactory, useClass: RateLimiterStoreFactory },\n {\n provide: RATE_LIMITER_TOKENS.Store,\n useFactory: (factory: RateLimiterStoreFactory) => factory.create(),\n inject: [RATE_LIMITER_TOKENS.StoreFactory],\n },\n ],\n})\nexport class RateLimiterModule implements OnInitialize, OnException {\n /**\n * Configure the rate limiter with a store choice.\n *\n * @param options - `{ store: 'kv', binding }` | `{ store: 'memory' }` | `{ store: { useClass } }`\n */\n static forRoot(options: RateLimiterModuleOptions): DynamicModule {\n return {\n module: RateLimiterModule,\n providers: [\n { provide: RATE_LIMITER_TOKENS.Options, useValue: options },\n ],\n }\n }\n\n /**\n * Async configuration. Use when store options depend on other services\n * (e.g. ConfigService).\n */\n static forRootAsync(options: AsyncModuleOptions<RateLimiterModuleOptions>): DynamicModule {\n return {\n module: RateLimiterModule,\n providers: [\n {\n provide: RATE_LIMITER_TOKENS.Options,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n\n /**\n * Eagerly resolve the store so a missing `forRoot()` fails at boot,\n * not on the first throttled request. Also registers a per-app marker\n * value so ThrottleMiddleware can distinguish \"module imported into this\n * app\" from \"module class loaded somewhere in the process\" (the latter is\n * indistinguishable at the DI layer because @Module's `registry()` call\n * binds providers globally at decoration time).\n */\n onInitialize({ container }: ModuleContext): void {\n container.resolve<IRateLimiterStore>(RATE_LIMITER_TOKENS.Store)\n container.registerValue(RATE_LIMITER_TOKENS.ModuleMarker, { imported: true })\n }\n\n /**\n * Inject `Retry-After` + `X-RateLimit-*` headers on every 429 response.\n * The body itself is rendered by `ExceptionHandler.defaultRender` —\n * content-negotiated, so HTML clients (browsers, Inertia) and JSON\n * clients both get the right shape.\n */\n onException(handler: ExceptionHandler): void {\n handler.dontReport([TooManyRequestsError])\n handler.respond((response, error) => {\n if (!(error instanceof TooManyRequestsError)) return response\n response.headers.set('Retry-After', String(error.info.retryAfter))\n response.headers.set('X-RateLimit-Limit', String(error.info.limit))\n response.headers.set('X-RateLimit-Remaining', '0')\n response.headers.set('X-RateLimit-Reset', String(Math.ceil(error.info.resetAt / 1000)))\n return response\n })\n }\n}\n"],"mappings":";;;;;;;;;AAwDO,IAAM,sBAAN,MAAM,4BAA4B,UAAU;CAID;CAHhD,4BAA6B,IAAI,IAA2B;CAE5D,YACE,WACA;EACA,MAAM;EAFwC,KAAA,YAAA;CAGhD;;;;;;;CAQA,IAAY,QAA2B;EACrC,OAAO,KAAK,UAAU,QAA2B,oBAAoB,KAAK;CAC5E;;;;;;CAOA,IAAI,MAAc,UAA+B;EAC/C,KAAK,UAAU,IAAI,MAAM,QAAQ;CACnC;CAEA,IAAI,MAAuB;EACzB,OAAO,KAAK,UAAU,IAAI,IAAI;CAChC;;;;;;;;;CAUA,MAAM,OAAO,MAAc,KAAoB,MAAsC;EACnF,MAAM,WAAW,KAAK,UAAU,IAAI,IAAI;EACxC,IAAI,CAAC,UACH,MAAM,IAAI,iBAAiB,iBAAiB,KAAK,kDAAkD,KAAK,yCAAyC;EAGnJ,MAAM,WAAW,MAAM,SAAS,GAAG;EAEnC,MAAM,UADS,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAC,QAAQ,EAAA,CACvC,QAAQ,MAAM,CAAC,EAAE,QAAQ;EAE/C,IAAI,OAAO,WAAW,GACpB,OAAO,KAAK;EAGd,IAAI;EACJ,IAAI;EAEJ,KAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,MAAM,KAAK,QAAQ,MAAM,MAAM,eAAe,MAAM,KAAK,MAAM,kBAAkB,KAAA,CAAS;GAChG,MAAM,MACJ,MAAM,kBAAkB,KAAA,IACpB,MAAM,KAAK,IAAI,KAAK,MAAM,aAAa,IACvC,MAAM,KAAK,YAAY,KAAK,MAAM,eAAe,MAAM,eAAe,MAAM,GAAG;GAErF,IAAI,IAAI,QAAQ,MAAM,KAAK;IACzB,IAAI,CAAC,YAAY,IAAI,UAAU,SAAS,SACtC,WAAW;KAAE;KAAO,SAAS,IAAI;IAAQ;IAE3C;GACF;GAEA,MAAM,YAAY,MAAM,MAAM,IAAI;GAClC,IAAI,CAAC,mBAAmB,YAAY,gBAAgB,WAClD,kBAAkB;IAAE;IAAO;IAAW,SAAS,IAAI;GAAQ;EAE/D;EAEA,IAAI,UAAU;GACZ,MAAM,UAAU,KAAK,YAAY,SAAS,MAAM,KAAK,GAAG,SAAS,OAAO;GACxE,IAAI,SAAS,MAAM,gBACjB,OAAO,SAAS,MAAM,eAAe,KAAK,OAAO;GAEnD,MAAM,IAAI,qBAAqB;IAC7B,YAAY,OAAO,QAAQ,cAAc;IACzC,OAAO,SAAS,MAAM;IACtB,SAAS,SAAS;GACpB,CAAC;EACH;EAEA,MAAM,KAAK;EAEX,IAAI,iBAAiB;GACnB,MAAM,UAAU,KAAK,YACnB,gBAAgB,MAAM,KACtB,gBAAgB,WAChB,gBAAgB,OAClB;GAMA,MAAM,aAAa,IAAI,EAAE;GACzB,WAAW,QAAQ,IAAI,qBAAqB,QAAQ,oBAAoB;GACxE,WAAW,QAAQ,IAAI,yBAAyB,QAAQ,wBAAwB;GAChF,WAAW,QAAQ,IAAI,qBAAqB,QAAQ,oBAAoB;EAC1E;CACF;;;;;CAMA,MAAc,IAAI,KAAa,eAA8C;EAC3E,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,WAAW,MAAM,KAAK,MAAM,IAAe,GAAG;EAEpD,IAAI;EACJ,IAAI,CAAC,YAAY,SAAS,WAAW,KACnC,OAAO;GAAE,OAAO;GAAG,SAAS,MAAM,gBAAgB;EAAK;OAEvD,OAAO;GAAE,OAAO,SAAS,QAAQ;GAAG,SAAS,SAAS;EAAQ;EAGhE,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,UAAU,OAAO,GAAI,CAAC;EACrE,MAAM,KAAK,MAAM,IAAI,KAAK,MAAM,UAAU;EAC1C,OAAO;CACT;;;;;;;;;;;;;;;;;;;;CAqBA,MAAc,YACZ,KACA,eACA,OACA,KACuB;EACvB,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,WAAW,MAAM,KAAK,MAAM,IAAoB,GAAG;EAEzD,IAAI,YAAY,SAAS,UAAU,KAAK;GACtC,IAAI,SAAS,OAAO,SAAS,KAAK,GAChC,OAAO;IAAE,OAAO,SAAS,OAAO;IAAQ,SAAS,SAAS;GAAQ;GAMpE,IAAI,SAAS,OAAO,UAAU,KAC5B,OAAO;IAAE,OAAO,SAAS,OAAO,SAAS;IAAG,SAAS,SAAS;GAAQ;GAGxE,MAAM,SAAS,CAAC,GAAG,SAAS,QAAQ,KAAK;GACzC,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,UAAU,OAAO,GAAI,CAAC;GACzE,MAAM,KAAK,MAAM,IAAI,KAAK;IAAE;IAAQ,SAAS,SAAS;GAAQ,GAAG,UAAU;GAC3E,OAAO;IAAE,OAAO,OAAO;IAAQ,SAAS,SAAS;GAAQ;EAC3D;EAEA,MAAM,UAAU,MAAM,gBAAgB;EACtC,MAAM,KAAK,MAAM,IAAI,KAAK;GAAE,QAAQ,CAAC,KAAK;GAAG;EAAQ,GAAG,KAAK,IAAI,GAAG,aAAa,CAAC;EAClF,OAAO;GAAE,OAAO;GAAG;EAAQ;CAC7B;;;;;;CAOA,QACE,MACA,eACA,IACA,UACQ;EAER,OAAO,MAAM,KAAK,GAAG,WAAW,OAAO,KAAK,cAAc,GAD5C,MAAM;CAEtB;CAEA,YAAoB,OAAe,WAAmB,SAAmC;EACvF,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,KAAK,IAAI,KAAK,GAAI,CAAC;EACvE,OAAO;GACL,eAAe,OAAO,UAAU;GAChC,qBAAqB,OAAO,KAAK;GACjC,yBAAyB,OAAO,KAAK,IAAI,GAAG,SAAS,CAAC;GACtD,qBAAqB,OAAO,KAAK,KAAK,UAAU,GAAI,CAAC;EACvD;CACF;AACF;AA7MC,sBAAA,WAAA,CAAA,UAAU,GAAA,gBAAA,GAKN,OAAO,UAAU,SAAS,CAAA,CAAA,GAAA,mBAAA;;;;;;;;;;;;;;;AC7C/B,IAAa,qBAAb,MAA6D;CAC9B;CAA7B,YAAY,OAAsC;EAArB,KAAA,QAAA;CAAsB;CAEnD,MAAM,IAAO,KAAgC;EAC3C,OAAQ,MAAM,KAAK,MAAM,IAAO,KAAK,MAAM,KAAM;CACnD;CAEA,MAAM,IAAO,KAAa,OAAU,YAAmC;EACrE,MAAM,MAAM,KAAK,IAAI,IAAI,KAAK,KAAK,UAAU,CAAC;EAG9C,MAAM,KAAK,MAAM,IAAI,KAAK,KAAK,UAAU,KAAK,GAAG,EAAE,eAAe,IAAI,CAAC;CACzE;CAEA,MAAM,OAAO,KAA4B;EACvC,MAAM,KAAK,MAAM,OAAO,GAAG;CAC7B;AACF;;;;;;;;;;;;AChBA,IAAa,2BAAb,MAAmE;CACjE,0BAA2B,IAAI,IAAmB;CAElD,IAAO,KAAgC;EACrC,MAAM,QAAQ,KAAK,QAAQ,IAAI,GAAG;EAClC,IAAI,CAAC,OAAO,OAAO,QAAQ,QAAQ,IAAI;EACvC,IAAI,MAAM,aAAa,KAAK,IAAI,GAAG;GACjC,KAAK,QAAQ,OAAO,GAAG;GACvB,OAAO,QAAQ,QAAQ,IAAI;EAC7B;EACA,OAAO,QAAQ,QAAQ,MAAM,KAAU;CACzC;CAEA,IAAO,KAAa,OAAU,YAAmC;EAC/D,KAAK,QAAQ,IAAI,KAAK;GAAE;GAAO,WAAW,KAAK,IAAI,IAAI,aAAa;EAAK,CAAC;EAC1E,OAAO,QAAQ,QAAQ;CACzB;CAEA,OAAO,KAA4B;EACjC,KAAK,QAAQ,OAAO,GAAG;EACvB,OAAO,QAAQ,QAAQ;CACzB;AACF;;;ACLO,IAAM,0BAAN,MAAM,wBAAwB;CAEiB;CACE;CACV;CAEzB;CALnB,YACE,KACA,OACA,WACA,SAEA;EALkD,KAAA,MAAA;EACE,KAAA,QAAA;EACV,KAAA,YAAA;EAEzB,KAAA,UAAA;CAChB;CAEH,SAA4B;EAC1B,IAAI,CAAC,KAAK,SACR,MAAM,IAAI,iBAAiB,mHAAmH;EAGhJ,MAAM,EAAE,UAAU,KAAK;EAEvB,IAAI,UAAU,UACZ,OAAO,IAAI,yBAAyB;EAGtC,IAAI,UAAU,MAAM;GAClB,MAAM,UAAU,KAAK,IAAI,KAAK,QAAQ;GACtC,IAAI,CAAC,SACH,MAAM,IAAI,iBAAiB,eAAe,OAAO,KAAK,QAAQ,OAAO,EAAE,uCAAuC;GAEhH,OAAO,IAAI,mBAAmB,KAAK,MAAM,YAAY,OAAO,CAAC;EAC/D;EAEA,OAAO,KAAK,UAAU,QAA2B,MAAM,QAAQ;CACjE;AACF;;CA/BC,UAAU;CAGN,gBAAA,GAAA,OAAO,UAAU,aAAa,CAAA;CAC9B,gBAAA,GAAA,OAAO,aAAa,YAAY,CAAA;CAChC,gBAAA,GAAA,OAAO,eAAe,CAAA;CACtB,gBAAA,GAAA,OAAO,oBAAoB,SAAS,EAAE,YAAY,KAAK,CAAC,CAAA;;;;;ACQtD,IAAM,oBAAA,qBAAN,MAAM,kBAAuD;;;;;;CAMlE,OAAO,QAAQ,SAAkD;EAC/D,OAAO;GACL,QAAA;GACA,WAAW,CACT;IAAE,SAAS,oBAAoB;IAAS,UAAU;GAAQ,CAC5D;EACF;CACF;;;;;CAMA,OAAO,aAAa,SAAsE;EACxF,OAAO;GACL,QAAA;GACA,WAAW,CACT;IACE,SAAS,oBAAoB;IAC7B,YAAY,QAAQ;IACpB,QAAQ,QAAQ;GAClB,CACF;EACF;CACF;;;;;;;;;CAUA,aAAa,EAAE,aAAkC;EAC/C,UAAU,QAA2B,oBAAoB,KAAK;EAC9D,UAAU,cAAc,oBAAoB,cAAc,EAAE,UAAU,KAAK,CAAC;CAC9E;;;;;;;CAQA,YAAY,SAAiC;EAC3C,QAAQ,WAAW,CAAC,oBAAoB,CAAC;EACzC,QAAQ,SAAS,UAAU,UAAU;GACnC,IAAI,EAAE,iBAAiB,uBAAuB,OAAO;GACrD,SAAS,QAAQ,IAAI,eAAe,OAAO,MAAM,KAAK,UAAU,CAAC;GACjE,SAAS,QAAQ,IAAI,qBAAqB,OAAO,MAAM,KAAK,KAAK,CAAC;GAClE,SAAS,QAAQ,IAAI,yBAAyB,GAAG;GACjD,SAAS,QAAQ,IAAI,qBAAqB,OAAO,KAAK,KAAK,MAAM,KAAK,UAAU,GAAI,CAAC,CAAC;GACtF,OAAO;EACT,CAAC;CACH;AACF;AA1EC,oBAAA,qBAAA,WAAA,CAAA,OAAO;CACN,SAAS,CAAC,WAAW;CACrB,WAAW;EACT;GAAE,SAAS,oBAAoB;GAAU,UAAU;EAAoB;EACvE;GAAE,SAAS,oBAAoB;GAAc,UAAU;EAAwB;EAC/E;GACE,SAAS,oBAAoB;GAC7B,aAAa,YAAqC,QAAQ,OAAO;GACjE,QAAQ,CAAC,oBAAoB,YAAY;EAC3C;CACF;AACF,CAAC,CAAA,GAAA,iBAAA"}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { Gr as PartitionResolver, Hr as CacheableOptions, Jn as AsyncModuleOptions, Jr as ResponseCacheGatewayOptions, Kr as PurgesCacheOptions, Ur as CachedEntrypointName, Wr as EntrypointNameFrom, Xn as DynamicModule, Yr as ResponseCacheModuleOptions, _r as WorkersCache, bn as GatewayRouteTable, hr as PurgeSpec, li as ApplicationError, qr as ResolvedCacheable, yn as GatewayRouteEntry } from "../index-B2WtEagR.mjs";
|
|
2
|
+
//#region src/response-cache/decorators/cacheable.decorator.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Mark a route's response as cacheable by Cloudflare Workers Caching.
|
|
5
|
+
*
|
|
6
|
+
* Only `GET` and `HEAD` are ever cached. Omitted fields fall back to the
|
|
7
|
+
* module's `defaults` block; an explicit value always wins.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* @Get('/blog/:slug')
|
|
12
|
+
* @Cacheable({ ttl: 300, swr: 60, tags: ['post:{param.slug}'] })
|
|
13
|
+
* async show(ctx: RouterContext) { ... }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Module defaults fill in whatever the route leaves out:
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* ResponseCacheModule.forRoot({ defaults: { ttl: 300, swr: 60 } })
|
|
21
|
+
*
|
|
22
|
+
* @Get('/pricing')
|
|
23
|
+
* @Cacheable() // ttl 300, swr 60
|
|
24
|
+
* async pricing(ctx: RouterContext) { ... }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* **A guarded route needs a `partitionBy`.** A guarded route's response differs
|
|
28
|
+
* per caller, so caching it under one shared entry would serve one user's
|
|
29
|
+
* response to another. `@Cacheable` on a route carrying `@UseGuards` is a boot
|
|
30
|
+
* error unless it declares a non-empty `partitionBy` (or inherits one from
|
|
31
|
+
* `defaults.partitionBy`) — and per-caller keying additionally requires
|
|
32
|
+
* `ResponseCacheModule.forRoot({ gateway: { entrypoint } })`, without which
|
|
33
|
+
* `partitionBy` itself is rejected at boot.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* @Get('/dashboard')
|
|
38
|
+
* @UseGuards(AuthGuard)
|
|
39
|
+
* @Cacheable({ ttl: 60, partitionBy: ['user'] })
|
|
40
|
+
* async dashboard(ctx: RouterContext) { ... }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function Cacheable(options?: CacheableOptions): MethodDecorator;
|
|
44
|
+
/** Read the `@Cacheable` options for a method. Returns `undefined` when absent. */
|
|
45
|
+
export declare function getCacheable(target: object, methodName: string): CacheableOptions | undefined;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/response-cache/decorators/purges-cache.decorator.d.ts
|
|
48
|
+
/**
|
|
49
|
+
* Invalidate cached responses after this route mutates data.
|
|
50
|
+
*
|
|
51
|
+
* Runs after the handler returns a 2xx or 3xx. `pathPrefixes` matches the
|
|
52
|
+
* request path only, so `['/blog']` also clears `/blog?page=2`.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* @Post('/posts/:slug/publish')
|
|
57
|
+
* @PurgesCache({ tags: ['post:{param.slug}', 'category:{data.post.categoryId}'] })
|
|
58
|
+
* async publish(ctx: RouterContext) { ... }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function PurgesCache(options: PurgesCacheOptions): MethodDecorator;
|
|
62
|
+
/** Read the `@PurgesCache` options for a method. Returns `undefined` when absent. */
|
|
63
|
+
export declare function getPurgesCache(target: object, methodName: string): PurgesCacheOptions | undefined;
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/response-cache/response-cache.module.d.ts
|
|
66
|
+
/**
|
|
67
|
+
* Response caching on Cloudflare Workers Caching — opt-in, NOT registered
|
|
68
|
+
* automatically by `Application`.
|
|
69
|
+
*
|
|
70
|
+
* Requires `cache.enabled = true` in the Wrangler config, Wrangler >= 4.69.0,
|
|
71
|
+
* and `compatibility_date >= 2026-07-06`. Without that binding, a route
|
|
72
|
+
* decorated with `@PurgesCache` throws `CachePurgeError` **at request time**
|
|
73
|
+
* — after its mutation has already committed — rather than silently
|
|
74
|
+
* skipping the purge and leaving the cache stale. Fail-loud is deliberate
|
|
75
|
+
* here, but it does mean a `@PurgesCache` route deployed to an environment
|
|
76
|
+
* that forgot `cache.enabled` turns every successful mutation into a `500`;
|
|
77
|
+
* confirm the binding is present in every environment that uses it.
|
|
78
|
+
*
|
|
79
|
+
* `forRootAsync`'s `useFactory` **must be synchronous**, or resolve
|
|
80
|
+
* synchronously from the caller's perspective: route registration reads
|
|
81
|
+
* `defaults` before any async work this module kicks off could settle, so
|
|
82
|
+
* an async factory's `Promise` is read as the options object itself,
|
|
83
|
+
* silently discarding `defaults` (this is detected and throws at boot,
|
|
84
|
+
* rather than silently caching without the intended defaults).
|
|
85
|
+
*
|
|
86
|
+
* Per-caller caching needs the gateway: `partitionBy`, `partitions`, and
|
|
87
|
+
* `primers` all throw `ResponseCacheConfigError` at boot unless
|
|
88
|
+
* `gateway: { entrypoint }` names a cached entrypoint built with
|
|
89
|
+
* `cachedEntrypoint(stratal)` from `stratal/workers`. A route that declares a
|
|
90
|
+
* partition it cannot honour must fail loudly rather than cache per-user data
|
|
91
|
+
* publicly.
|
|
92
|
+
*
|
|
93
|
+
* ```typescript
|
|
94
|
+
* @Module({
|
|
95
|
+
* imports: [ResponseCacheModule.forRoot({
|
|
96
|
+
* defaults: { ttl: 300, swr: 60, vary: ['Accept-Language'] },
|
|
97
|
+
* })],
|
|
98
|
+
* })
|
|
99
|
+
* export class AppModule {}
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* With per-caller partitioning:
|
|
103
|
+
*
|
|
104
|
+
* ```typescript
|
|
105
|
+
* // app.module.ts
|
|
106
|
+
* ResponseCacheModule.forRoot({
|
|
107
|
+
* gateway: { entrypoint: 'Cached' },
|
|
108
|
+
* partitions: { user: (ctx) => ctx.user().id },
|
|
109
|
+
* })
|
|
110
|
+
*
|
|
111
|
+
* // src/index.ts
|
|
112
|
+
* const stratal = new Stratal({ module: AppModule })
|
|
113
|
+
* export default stratal
|
|
114
|
+
* export const Cached = cachedEntrypoint(stratal)
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export declare class ResponseCacheModule {
|
|
118
|
+
static forRoot(options?: ResponseCacheModuleOptions): DynamicModule;
|
|
119
|
+
static forRootAsync(options: AsyncModuleOptions<ResponseCacheModuleOptions>): DynamicModule;
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/response-cache/response-cache.tokens.d.ts
|
|
123
|
+
export declare const RESPONSE_CACHE_TOKENS: {
|
|
124
|
+
readonly Options: symbol;
|
|
125
|
+
readonly ResponseCacheService: symbol;
|
|
126
|
+
readonly CacheabilityService: symbol;
|
|
127
|
+
readonly PartitionResolverService: symbol;
|
|
128
|
+
readonly GatewayPrimerService: symbol;
|
|
129
|
+
readonly GatewayPrimers: symbol;
|
|
130
|
+
readonly GatewayRouteTable: symbol;
|
|
131
|
+
};
|
|
132
|
+
type ResponseCacheToken = (typeof RESPONSE_CACHE_TOKENS)[keyof typeof RESPONSE_CACHE_TOKENS];
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/response-cache/errors/response-cache.error.d.ts
|
|
135
|
+
/** A rendered `Cache-Tag` value Cloudflare would silently drop. */
|
|
136
|
+
export declare class InvalidCacheTagError extends ApplicationError {
|
|
137
|
+
constructor(tag: string, reason: string);
|
|
138
|
+
}
|
|
139
|
+
/** A purge failed after its mutation already committed. */
|
|
140
|
+
export declare class CachePurgeError extends ApplicationError {
|
|
141
|
+
constructor(detail: string, cause?: unknown);
|
|
142
|
+
}
|
|
143
|
+
/** Misconfiguration detected at boot. */
|
|
144
|
+
export declare class ResponseCacheConfigError extends ApplicationError {
|
|
145
|
+
constructor(detail: string);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/response-cache/resolve-cacheable.d.ts
|
|
149
|
+
/** Where a `@Cacheable` was declared, for error messages and the guard rule. */
|
|
150
|
+
interface CacheableContext {
|
|
151
|
+
controller: string;
|
|
152
|
+
method: string;
|
|
153
|
+
/** True when the route or its controller has guards attached. */
|
|
154
|
+
guarded: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* The `:param` names this route's own path declares, in order — used to
|
|
157
|
+
* validate `{param.*}` cache tags against a route that can never populate
|
|
158
|
+
* them. `undefined` (rather than `[]`) means "not known to the caller",
|
|
159
|
+
* which skips that validation entirely instead of treating every
|
|
160
|
+
* `{param.*}` tag as unresolvable; only the real HTTP registration path
|
|
161
|
+
* (`RouteRegistrationService`) can supply it, so unit tests exercising
|
|
162
|
+
* `bindRouteCache` directly are free to omit it.
|
|
163
|
+
*/
|
|
164
|
+
routeParams?: string[];
|
|
165
|
+
/**
|
|
166
|
+
* True when `ResponseCacheModule.forRoot({ gateway: { entrypoint } })` names
|
|
167
|
+
* a cached entrypoint — the only configuration under which a partition can
|
|
168
|
+
* actually be placed in the cache key. Governs both the `partitionBy` throw
|
|
169
|
+
* in `bindRouteCache` and the guarded-route message below.
|
|
170
|
+
*/
|
|
171
|
+
gatewayConfigured?: boolean;
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/response-cache/boot-check.d.ts
|
|
175
|
+
/**
|
|
176
|
+
* Why this entrypoint cannot cache, or `undefined` when it can.
|
|
177
|
+
*
|
|
178
|
+
* A Worker cannot read its own Wrangler configuration, but the presence of
|
|
179
|
+
* `ctx.cache` tells us whether Workers Caching is enabled for this entrypoint.
|
|
180
|
+
*
|
|
181
|
+
* Reported rather than thrown, and the distinction is the whole point. The
|
|
182
|
+
* hazard is a route stamping `public, max-age=…` while nothing stores the
|
|
183
|
+
* response: a claim the app makes and no layer honours. Refusing to make that
|
|
184
|
+
* claim removes the hazard — the response falls through to
|
|
185
|
+
* `createNoStoreFallbackMiddleware` and is stamped `private, no-store`, which
|
|
186
|
+
* is both true and visible in the response itself. Failing the request
|
|
187
|
+
* removes it too, but takes down every route in the isolate to do it,
|
|
188
|
+
* including the ones that never asked to be cached.
|
|
189
|
+
*
|
|
190
|
+
* The answer belongs to an entrypoint rather than to the app, because a
|
|
191
|
+
* gateway topology runs two of them — one with `cache.enabled` false by
|
|
192
|
+
* design, one with it true — inside a single isolate.
|
|
193
|
+
*/
|
|
194
|
+
export declare function cachingUnavailableReason(cacheableRouteCount: number, cache: WorkersCache | undefined): string | undefined;
|
|
195
|
+
/**
|
|
196
|
+
* Validate the configured entrypoint name itself.
|
|
197
|
+
*
|
|
198
|
+
* Split from the reachability check in `resolveCachedEntrypoint` because this
|
|
199
|
+
* part needs no runtime: a name that is empty, or names the gateway itself, is
|
|
200
|
+
* wrong on inspection and should never reach a first request.
|
|
201
|
+
*/
|
|
202
|
+
export declare function assertValidGatewayEntrypoint(options: ResponseCacheModuleOptions): void;
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/response-cache/services/route-cache-binding.d.ts
|
|
205
|
+
/** What one route needs at request time, resolved once at registration. */
|
|
206
|
+
interface RouteCacheBinding {
|
|
207
|
+
cacheable?: ResolvedCacheable;
|
|
208
|
+
purges?: PurgesCacheOptions;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Fold a route's decorators and the module defaults into the config the request
|
|
212
|
+
* path uses. Runs once per route at registration, never per request.
|
|
213
|
+
*
|
|
214
|
+
* Returns `undefined` when the route declares neither decorator, so the hot path
|
|
215
|
+
* can skip all cache work with a single truthiness check.
|
|
216
|
+
*
|
|
217
|
+
* @throws {ResponseCacheConfigError} On a non-empty `partitionBy` when no
|
|
218
|
+
* `gateway.entrypoint` is configured. Placing a partition in the cache key
|
|
219
|
+
* requires the gateway to forward the read to a cached entrypoint over
|
|
220
|
+
* `ctx.exports`. Accepting the option and ignoring it would give every
|
|
221
|
+
* visitor a single shared entry for a route the author explicitly marked
|
|
222
|
+
* per-user.
|
|
223
|
+
* @throws {ResponseCacheConfigError} On a `{body.*}` tag in either decorator's
|
|
224
|
+
* `tags` — see `assertNoBodyScopeTags`.
|
|
225
|
+
* @throws {ResponseCacheConfigError} On a `{param.X}` tag whose `X` is not a
|
|
226
|
+
* `:param` in this route's own path (when `context.routeParams` is
|
|
227
|
+
* supplied) — see `assertParamTagsResolvable`.
|
|
228
|
+
* @throws {ResponseCacheConfigError} On a `{`/`}` in `@PurgesCache`'s
|
|
229
|
+
* `pathPrefixes` — see `assertNoTemplatedPathPrefixes`.
|
|
230
|
+
*/
|
|
231
|
+
export declare function bindRouteCache(cacheable: CacheableOptions | undefined, purges: PurgesCacheOptions | undefined, defaults: Omit<CacheableOptions, 'tags'>, context: CacheableContext): RouteCacheBinding | undefined;
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/response-cache/gateway-mode.d.ts
|
|
234
|
+
/**
|
|
235
|
+
* Mark an execution context as running in gateway mode.
|
|
236
|
+
*
|
|
237
|
+
* Called by `Stratal.fetch` (the default export — the eyeball entrypoint) and
|
|
238
|
+
* by `@stratal/testing`, which drives `HonoApp#fetch` directly. Deliberately
|
|
239
|
+
* **not** called by `cachedEntrypoint`, which is what stops the cached
|
|
240
|
+
* entrypoint from dispatching back into itself.
|
|
241
|
+
*
|
|
242
|
+
* Returns the same object it was given, so it can be used inline.
|
|
243
|
+
*/
|
|
244
|
+
export declare function markGatewayMode<T extends object>(ctx: T): T;
|
|
245
|
+
/** Whether this execution context was marked by {@link markGatewayMode}. */
|
|
246
|
+
export declare function isGatewayMode(ctx: unknown): boolean;
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/response-cache/gateway-binding.d.ts
|
|
249
|
+
/**
|
|
250
|
+
* The two things the gateway asks of the cached entrypoint.
|
|
251
|
+
*
|
|
252
|
+
* `ctx.exports.<Name>` is typed as `Cloudflare.Exports[Name]`, which resolves
|
|
253
|
+
* to `{}` unless the consumer has generated `Cloudflare.GlobalProps` with
|
|
254
|
+
* `wrangler types`. The framework can't require that, and can't name the
|
|
255
|
+
* consumer's export at compile time either — the name arrives as a string in
|
|
256
|
+
* `gateway.entrypoint` — so the stub is described structurally here and the
|
|
257
|
+
* lookup is checked at runtime by `resolveCachedEntrypoint`.
|
|
258
|
+
*/
|
|
259
|
+
interface CachedEntrypointStub {
|
|
260
|
+
/** The loopback `fetch`, routed through the cached entrypoint's cache. */
|
|
261
|
+
fetch(request: Request): Promise<Response>;
|
|
262
|
+
/**
|
|
263
|
+
* The RPC purge. Purges are scoped to the entrypoint that issues them, and
|
|
264
|
+
* mutations run inline in the *gateway*, whose cache is disabled — so a
|
|
265
|
+
* purge issued there would target an empty cache and silently do nothing.
|
|
266
|
+
* See `cachedEntrypoint`.
|
|
267
|
+
*/
|
|
268
|
+
purge(spec: PurgeSpec): Promise<{
|
|
269
|
+
success: boolean;
|
|
270
|
+
} | void>;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A loopback binding: callable to bind `ctx.props` for the invocation, and
|
|
274
|
+
* directly usable when no props are needed.
|
|
275
|
+
*
|
|
276
|
+
* `LoopbackServiceStub<T>` in `@cloudflare/workers-types` is
|
|
277
|
+
* `Fetcher<T> & ((opts: { props?: Props }) => Fetcher<T>)` — the callable form
|
|
278
|
+
* is the documented way to choose the callee's `ctx.props`, and `ctx.props` is
|
|
279
|
+
* what places a partition in the cache key.
|
|
280
|
+
*/
|
|
281
|
+
type CachedEntrypointBinding = CachedEntrypointStub & ((options: {
|
|
282
|
+
props?: Record<string, string>;
|
|
283
|
+
}) => CachedEntrypointStub);
|
|
284
|
+
/**
|
|
285
|
+
* Look up the configured cached entrypoint on `ctx.exports`, failing loudly if
|
|
286
|
+
* it isn't there.
|
|
287
|
+
*
|
|
288
|
+
* A typo in `gateway: { entrypoint: 'Cachd' }`, a missing `export const
|
|
289
|
+
* Cached = ...`, or a missing `enable_ctx_exports` compatibility flag would
|
|
290
|
+
* otherwise all present identically: every partitioned route quietly runs
|
|
291
|
+
* inline in the gateway forever, never caching, with no signal anywhere. The
|
|
292
|
+
* whole point of `partitionBy` refusing to boot without a gateway is that a
|
|
293
|
+
* declared partition is either honoured or it is an error — so this is an
|
|
294
|
+
* error too.
|
|
295
|
+
*
|
|
296
|
+
* @throws {ResponseCacheConfigError} `ctx.exports` is unavailable, or carries
|
|
297
|
+
* no export under `name`.
|
|
298
|
+
*/
|
|
299
|
+
export declare function resolveCachedEntrypoint(ctx: unknown, name: string): CachedEntrypointBinding;
|
|
300
|
+
/**
|
|
301
|
+
* A `WorkersCache` that purges through the cached entrypoint over RPC.
|
|
302
|
+
*
|
|
303
|
+
* Handed to `ResponseCacheService.purge()` in place of the gateway's own
|
|
304
|
+
* `ctx.cache` whenever a gateway is configured and this request is running as
|
|
305
|
+
* the gateway. See `cachedEntrypoint` for why that redirection is required
|
|
306
|
+
* rather than merely tidy.
|
|
307
|
+
*/
|
|
308
|
+
export declare function createLoopbackPurgeTarget(ctx: unknown, name: string): {
|
|
309
|
+
purge: (spec: PurgeSpec) => Promise<void | {
|
|
310
|
+
success: boolean;
|
|
311
|
+
}>;
|
|
312
|
+
};
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/response-cache/cached-entrypoint.d.ts
|
|
315
|
+
/**
|
|
316
|
+
* Whether the gateway should forward this request into the cached entrypoint.
|
|
317
|
+
*
|
|
318
|
+
* Anything that returns `false` runs inline in the gateway and never reaches
|
|
319
|
+
* the cached entrypoint at all — which is what keeps RFC 9111 heuristic
|
|
320
|
+
* freshness (a `200` with no `Cache-Control` cached for two hours) away from
|
|
321
|
+
* un-annotated routes.
|
|
322
|
+
*/
|
|
323
|
+
export declare function shouldLoopback(method: string, cacheable: CacheableOptions | undefined): boolean;
|
|
324
|
+
//#endregion
|
|
325
|
+
export { type CacheableContext, type CacheableOptions, type CachedEntrypointBinding, type CachedEntrypointName, type CachedEntrypointStub, type EntrypointNameFrom, type GatewayRouteEntry, GatewayRouteTable, type PartitionResolver, type PurgeSpec, type PurgesCacheOptions, type ResolvedCacheable, type ResponseCacheGatewayOptions, type ResponseCacheModuleOptions, type ResponseCacheToken, type RouteCacheBinding, type WorkersCache };
|
|
326
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/response-cache/decorators/cacheable.decorator.ts","../../src/response-cache/decorators/purges-cache.decorator.ts","../../src/response-cache/response-cache.module.ts","../../src/response-cache/response-cache.tokens.ts","../../src/response-cache/errors/response-cache.error.ts","../../src/response-cache/resolve-cacheable.ts","../../src/response-cache/boot-check.ts","../../src/response-cache/services/route-cache-binding.ts","../../src/response-cache/gateway-mode.ts","../../src/response-cache/gateway-binding.ts","../../src/response-cache/cached-entrypoint.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA8CgB,UAAU,UAAS,mBAAwB;;wBAO3C,aAAa,gBAAgB,qBAAqB;;;;;;;;;;;;;;;;wBCjClD,YAAY,SAAS,qBAAqB;;wBAqB1C,eAAe,gBAAgB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBC0BvD;SACJ,QAAQ,UAAS,6BAAkC;SASnD,aAAa,SAAS,mBAAmB,8BAA8B;;;;qBC7EnE;WACX;WACA;WACA;WACA;WACA;WACA;WACA;;KAGU,6BACF,oCAAoC;;;;qBCRjC,6BAA6B;EACxC,YAAY,aAAa;;;qBAMd,wBAAwB;EACnC,YAAY,gBAAgB;;;qBAMjB,iCAAiC;EAC5C,YAAY;;;;;UCdG;EACf;EACA;;EAEA;;;;;;;;;;EAUA;;;;;;;EAOA;;;;;;;;;;;;;;;;;;;;;;;wBCFc,yBACd,6BACA,OAAO;;;;;;;;wBAgCO,6BAA6B,SAAS;;;;UCpDrC;EACf,YAAY;EACZ,SAAS;;;;;;;;;;;;;;;;;;;;;;;wBA+KK,eACd,WAAW,8BACX,QAAQ,gCACR,UAAU,KAAK,2BACf,SAAS,mBACR;;;;;;;;;;;;;wBClJa,gBAAgB,kBAAkB,KAAK,IAAI;;wBAM3C,cAAc;;;;;;;;;;;;;UClCb;;EAEf,MAAM,SAAS,UAAU,QAAQ;;;;;;;EAOjC,MAAM,MAAM,YAAY;IAAU;;;;;;;;;;;;KAYxB,0BAA0B,yBAClC;EAAW,QAAQ;MAA6B;;;;;;;;;;;;;;;;wBAiBpC,wBAAwB,cAAc,eAAe;;;;;;;;;wBA+CrD,0BAA0B,cAAc;EAKpD,QAAK,MAAe,cAAS;IAlFY;;;;;;;;;;;;;wBCT7B,eAAe,gBAAgB,WAAW"}
|