theokit 0.2.4 → 0.4.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/dist/{actions-virtual-module-HWNTIEPI.js → actions-virtual-module-IY46EEEX.js} +2 -2
- package/dist/{actions-virtual-module-SEIPACG5.js → actions-virtual-module-XNHDNCZ6.js} +2 -2
- package/dist/add-2ZFFGGE4.js +0 -0
- package/dist/{app-typed-client-6GC6VWXS.js → app-typed-client-OUJO3V5J.js} +10 -5
- package/dist/{app-typed-client-6GC6VWXS.js.map → app-typed-client-OUJO3V5J.js.map} +1 -1
- package/dist/{app-typed-client-US2ZXBEC.js → app-typed-client-YOMWSA3F.js} +11 -6
- package/dist/{app-typed-client-US2ZXBEC.js.map → app-typed-client-YOMWSA3F.js.map} +1 -1
- package/dist/audit-log-BQWM5YLG.d.ts +60 -0
- package/dist/{aws-lambda-XYCGZH3I.js → aws-lambda-GKSTX6HD.js} +3 -3
- package/dist/body-parser-web-MUWBKZ3F.js +70 -0
- package/dist/body-parser-web-MUWBKZ3F.js.map +1 -0
- package/dist/broadcast-QOQGUNNK.js +0 -0
- package/dist/{build-MREW6MVS.js → build-D4J7XECU.js} +33 -24
- package/dist/build-D4J7XECU.js.map +1 -0
- package/dist/build-request-body-preview-II2235E6.js +0 -0
- package/dist/{bun-K73TQEWC.js → bun-ISHSNFIO.js} +3 -3
- package/dist/{check-PZR67OY5.js → check-NXYPLM6M.js} +2 -2
- package/dist/{chunk-WZ7CPVQB.js → chunk-27LWMYNK.js} +28 -24
- package/dist/chunk-27LWMYNK.js.map +1 -0
- package/dist/{chunk-5OKZY2VL.js → chunk-2F4FROEQ.js} +1695 -1522
- package/dist/chunk-2F4FROEQ.js.map +1 -0
- package/dist/chunk-4HBI7DHP.js +20 -0
- package/dist/chunk-4HBI7DHP.js.map +1 -0
- package/dist/{chunk-X4JAX2U3.js → chunk-4S2UCILN.js} +180 -125
- package/dist/chunk-4S2UCILN.js.map +1 -0
- package/dist/{chunk-C52GSJPF.js → chunk-4S6YHNG2.js} +11 -8
- package/dist/chunk-4S6YHNG2.js.map +1 -0
- package/dist/chunk-5ODOE6EF.js +0 -0
- package/dist/{chunk-KJVEJILQ.js → chunk-5PU65T5W.js} +9 -3
- package/dist/chunk-5PU65T5W.js.map +1 -0
- package/dist/chunk-5QW7IQQU.js +36 -0
- package/dist/chunk-5QW7IQQU.js.map +1 -0
- package/dist/chunk-5Z2727GV.js +1324 -0
- package/dist/chunk-5Z2727GV.js.map +1 -0
- package/dist/{chunk-YLBSSEHX.js → chunk-6NEXVBPY.js} +5 -15
- package/dist/chunk-6NEXVBPY.js.map +1 -0
- package/dist/chunk-7MQOHNHE.js +36 -0
- package/dist/chunk-7MQOHNHE.js.map +1 -0
- package/dist/{chunk-TPCT3MXV.js → chunk-ABVITXFH.js} +405 -272
- package/dist/chunk-ABVITXFH.js.map +1 -0
- package/dist/chunk-ASDXVRJD.js +185 -0
- package/dist/chunk-ASDXVRJD.js.map +1 -0
- package/dist/chunk-B3L3TJVF.js +406 -0
- package/dist/chunk-B3L3TJVF.js.map +1 -0
- package/dist/{chunk-PB4GR7EP.js → chunk-C3ZZ56YZ.js} +1 -18
- package/dist/chunk-C3ZZ56YZ.js.map +1 -0
- package/dist/{chunk-O7335ZGH.js → chunk-CG563V5M.js} +5 -3
- package/dist/chunk-CG563V5M.js.map +1 -0
- package/dist/{chunk-5OFPQK6N.js → chunk-COXLEZCN.js} +2 -1
- package/dist/chunk-COXLEZCN.js.map +1 -0
- package/dist/{chunk-O4BLVPML.js → chunk-DNMSV3R3.js} +22 -28
- package/dist/chunk-DNMSV3R3.js.map +1 -0
- package/dist/chunk-E3JH6YUM.js +1 -0
- package/dist/chunk-ESC54TAK.js +220 -0
- package/dist/chunk-ESC54TAK.js.map +1 -0
- package/dist/chunk-FI7MPTJW.js +77 -0
- package/dist/chunk-FI7MPTJW.js.map +1 -0
- package/dist/chunk-H4J2LECY.js +201 -0
- package/dist/chunk-H4J2LECY.js.map +1 -0
- package/dist/chunk-IAJ2JVEH.js +256 -0
- package/dist/chunk-IAJ2JVEH.js.map +1 -0
- package/dist/{chunk-F3TG5FJV.js → chunk-IEZCAT2I.js} +7 -1
- package/dist/{chunk-F3TG5FJV.js.map → chunk-IEZCAT2I.js.map} +1 -1
- package/dist/chunk-JHEAWR3L.js +1 -0
- package/dist/chunk-JQSKBMXP.js +17 -0
- package/dist/chunk-JQSKBMXP.js.map +1 -0
- package/dist/{chunk-4QTKSLTO.js → chunk-K4M64WD6.js} +9 -5
- package/dist/{chunk-4QTKSLTO.js.map → chunk-K4M64WD6.js.map} +1 -1
- package/dist/chunk-KI7OWQUX.js +293 -0
- package/dist/chunk-KI7OWQUX.js.map +1 -0
- package/dist/chunk-KT3MWNZG.js +0 -0
- package/dist/{chunk-ZJW5FFYJ.js → chunk-LC5PYNJP.js} +2 -2
- package/dist/{chunk-X2OLD264.js → chunk-M2EY7KDR.js} +1228 -1124
- package/dist/chunk-M2EY7KDR.js.map +1 -0
- package/dist/{chunk-YWWHK7R6.js → chunk-MR7OLIC6.js} +3 -3
- package/dist/chunk-NM4WF3XD.js +63 -0
- package/dist/chunk-NM4WF3XD.js.map +1 -0
- package/dist/chunk-NPMCKOX4.js +1 -0
- package/dist/{chunk-VRHXOKRP.js → chunk-NZXH2LQQ.js} +86 -83
- package/dist/chunk-NZXH2LQQ.js.map +1 -0
- package/dist/{chunk-2BQYEBCK.js → chunk-OLPB36O2.js} +87 -5
- package/dist/chunk-OLPB36O2.js.map +1 -0
- package/dist/chunk-PIVX3DYW.js +142 -0
- package/dist/chunk-PIVX3DYW.js.map +1 -0
- package/dist/{chunk-7TOMTMHT.js → chunk-R7OC6UDK.js} +2 -1
- package/dist/chunk-R7OC6UDK.js.map +1 -0
- package/dist/chunk-RESN62GB.js +269 -0
- package/dist/chunk-RESN62GB.js.map +1 -0
- package/dist/{chunk-64JI5LTO.js → chunk-RMU7EBRO.js} +2 -2
- package/dist/chunk-RMU7EBRO.js.map +1 -0
- package/dist/chunk-TQYSPYKU.js +131 -0
- package/dist/chunk-TQYSPYKU.js.map +1 -0
- package/dist/chunk-TSLSIRBR.js +107 -0
- package/dist/chunk-TSLSIRBR.js.map +1 -0
- package/dist/{chunk-XP7YXRHO.js → chunk-U6TPSW4L.js} +37 -5
- package/dist/chunk-U6TPSW4L.js.map +1 -0
- package/dist/chunk-UD3LFGDL.js +45 -0
- package/dist/chunk-UD3LFGDL.js.map +1 -0
- package/dist/chunk-UUFYP2UM.js +161 -0
- package/dist/chunk-UUFYP2UM.js.map +1 -0
- package/dist/{chunk-SNDZQ64K.js → chunk-VBZCVFSA.js} +85 -3
- package/dist/chunk-VBZCVFSA.js.map +1 -0
- package/dist/chunk-VMEWD57H.js +137 -0
- package/dist/chunk-VMEWD57H.js.map +1 -0
- package/dist/{chunk-BE2LKDI7.js → chunk-WEGALZEU.js} +3 -3
- package/dist/{chunk-ZOXNJV2O.js → chunk-WGHJD6I7.js} +35 -138
- package/dist/chunk-WGHJD6I7.js.map +1 -0
- package/dist/chunk-XMS5VRIK.js +0 -0
- package/dist/chunk-Y4H3JNVK.js +18 -0
- package/dist/chunk-Y4H3JNVK.js.map +1 -0
- package/dist/chunk-Z5IGLBWE.js +329 -0
- package/dist/chunk-Z5IGLBWE.js.map +1 -0
- package/dist/chunk-ZEGYW52B.js +26 -0
- package/dist/chunk-ZEGYW52B.js.map +1 -0
- package/dist/chunk-ZJQ4O76G.js +87 -0
- package/dist/chunk-ZJQ4O76G.js.map +1 -0
- package/dist/cli/index.js +44 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.d.ts +107 -1
- package/dist/client/index.js +152 -6
- package/dist/client/index.js.map +1 -1
- package/dist/{cloudflare-LCAOTRYZ.js → cloudflare-OJGJ7R2D.js} +3 -3
- package/dist/configure-agent-registry-6YGTJYBC.js +0 -0
- package/dist/cookies-MJKMGJ7N.js +22 -0
- package/dist/csrf-BBrEZSBW.d.ts +107 -0
- package/dist/csrf-readiness-store-CjIoub3U.d.ts +43 -0
- package/dist/define-websocket-CdK94O-D.d.ts +64 -0
- package/dist/{deno-deploy-ZN4RE5HF.js → deno-deploy-BHWKFTOW.js} +3 -3
- package/dist/{dev-6P2DM33Q.js → dev-MJVSRZGF.js} +13 -13
- package/dist/{dev-emit-MLDFOLUU.js → dev-emit-5VPRCHOD.js} +39 -142
- package/dist/dev-emit-5VPRCHOD.js.map +1 -0
- package/dist/{dev-emit-PRSRPDHA.js → dev-emit-HHP2XJXE.js} +5 -5
- package/dist/devtools/entry.js +185 -131
- package/dist/devtools/entry.js.map +1 -1
- package/dist/{dispatcher-AQJGI3HU.js → dispatcher-63LBXYLE.js} +2 -2
- package/dist/{dispatcher-E6QV32BO.js → dispatcher-EJME6C6M.js} +5 -2
- package/dist/dispatcher-EJME6C6M.js.map +1 -0
- package/dist/{dist-E6BL4ZVY.js → dist-KRW6OVD4.js} +7 -7
- package/dist/dist-KRW6OVD4.js.map +1 -0
- package/dist/docker-EZDA5FT7.js +0 -0
- package/dist/error-envelope-BsNzzAV5.d.ts +62 -0
- package/dist/{generate-DT4IIAHF.js → generate-AZ2K6Z2Z.js} +75 -2
- package/dist/generate-AZ2K6Z2Z.js.map +1 -0
- package/dist/index-DWWEdFh5.d.ts +399 -0
- package/dist/index.d.ts +161 -1391
- package/dist/index.js +20 -8
- package/dist/index.js.map +1 -1
- package/dist/{info-FJ4NXKTB.js → info-47VB62MV.js} +5 -5
- package/dist/job-backend-CgC8Xf33.d.ts +68 -0
- package/dist/{lib-NL5JXGEB.js → lib-NST3PNZX.js} +31 -31
- package/dist/lib-NST3PNZX.js.map +1 -0
- package/dist/module-loader-Cfz7dgCP.d.ts +26 -0
- package/dist/{netlify-5VQ22Z2P.js → netlify-GSNSJGMN.js} +3 -3
- package/dist/{node-3APTLGWB.js → node-6I5B6RL3.js} +3 -3
- package/dist/node-6I5B6RL3.js.map +1 -0
- package/dist/{openapi-SOKZTFE3.js → openapi-NFLSNNVQ.js} +10 -10
- package/dist/plugin-runner-BGBkzgi0.d.ts +95 -0
- package/dist/plugin-types-DNJGxr4Z.d.ts +79 -0
- package/dist/{proper-lockfile-4Y3DP3RP.js → proper-lockfile-MVKMQGFK.js} +27 -27
- package/dist/proper-lockfile-MVKMQGFK.js.map +1 -0
- package/dist/rate-limit-BdNDZ3vt.d.ts +58 -0
- package/dist/registry-QHEZ3BWB.js +25 -0
- package/dist/router-RGZFX75G.js +274 -0
- package/dist/router-RGZFX75G.js.map +1 -0
- package/dist/{routes-XVRAUH6H.js → routes-3A4XGIEJ.js} +6 -6
- package/dist/{scan-C2BOKLSY.js → scan-NQFI5S7P.js} +2 -2
- package/dist/scan-NQFI5S7P.js.map +1 -0
- package/dist/schema-D3v8VB7o.d.ts +76 -0
- package/dist/{schema-VR6YNVLQ.js → schema-KZVOV333.js} +5 -3
- package/dist/schema-KZVOV333.js.map +1 -0
- package/dist/server/agent/index.d.ts +229 -0
- package/dist/server/agent/index.js +16 -0
- package/dist/server/agent/index.js.map +1 -0
- package/dist/server/auth/index.d.ts +46 -1
- package/dist/server/auth/index.js +10 -3
- package/dist/server/cost/index.d.ts +1 -3
- package/dist/server/cost/index.js +1 -1
- package/dist/server/cron/index.js +4 -2
- package/dist/server/define/index.d.ts +281 -0
- package/dist/server/define/index.js +26 -0
- package/dist/server/define/index.js.map +1 -0
- package/dist/server/http/index.d.ts +10 -0
- package/dist/server/http/index.js +74 -0
- package/dist/server/http/index.js.map +1 -0
- package/dist/server/index.d.ts +151 -1677
- package/dist/server/index.js +558 -1102
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs/index.d.ts +348 -2
- package/dist/server/jobs/index.js +5 -3
- package/dist/server/observability/index.d.ts +324 -0
- package/dist/server/observability/index.js +48 -0
- package/dist/server/observability/index.js.map +1 -0
- package/dist/server/plugins/index.d.ts +17 -0
- package/dist/server/plugins/index.js +17 -0
- package/dist/server/plugins/index.js.map +1 -0
- package/dist/server/rate-limit/index.d.ts +105 -0
- package/dist/server/rate-limit/index.js +25 -0
- package/dist/server/rate-limit/index.js.map +1 -0
- package/dist/server/realtime/index.d.ts +15 -0
- package/dist/server/realtime/index.js +8 -0
- package/dist/server/realtime/index.js.map +1 -0
- package/dist/server/scan/index.d.ts +106 -0
- package/dist/server/scan/index.js +43 -0
- package/dist/server/scan/index.js.map +1 -0
- package/dist/server/security/index.d.ts +193 -0
- package/dist/server/security/index.js +50 -0
- package/dist/server/security/index.js.map +1 -0
- package/dist/server/storage/index.d.ts +22 -0
- package/dist/server/storage/index.js +14 -0
- package/dist/server/storage/index.js.map +1 -0
- package/dist/server/webhook/index.d.ts +148 -0
- package/dist/server/webhook/index.js +19 -0
- package/dist/server/webhook/index.js.map +1 -0
- package/dist/server-error-to-envelope-NO4CCAFQ.js +8 -0
- package/dist/server-error-to-envelope-NO4CCAFQ.js.map +1 -0
- package/dist/server-error-to-envelope-UJK6OWDJ.js +134 -0
- package/dist/server-error-to-envelope-UJK6OWDJ.js.map +1 -0
- package/dist/server-routes-hmr-GZJGPWMS.js +44 -0
- package/dist/server-routes-hmr-GZJGPWMS.js.map +1 -0
- package/dist/server-routes-hmr-PBHSKCQJ.js +42 -0
- package/dist/server-routes-hmr-PBHSKCQJ.js.map +1 -0
- package/dist/services-json-Z2QNGVPW.js +142 -0
- package/dist/services-json-Z2QNGVPW.js.map +1 -0
- package/dist/{services-typed-client-XWYYBWGW.js → services-typed-client-KK6RHRDG.js} +2 -2
- package/dist/{services-typed-client-ZX5JUHH3.js → services-typed-client-T7M5VPBP.js} +2 -2
- package/dist/{sqlite-vec-ARPNUBWT.js → sqlite-vec-54KB4RD3.js} +2 -2
- package/dist/sqlite-vec-54KB4RD3.js.map +1 -0
- package/dist/{start-R7XSQL5L.js → start-CGSZPBAG.js} +23 -23
- package/dist/start-CGSZPBAG.js.map +1 -0
- package/dist/{static-TZBVBDTB.js → static-QI5NQT2X.js} +6 -6
- package/dist/storage-manager-C4jsO0Tp.d.ts +89 -0
- package/dist/storage-manager-D5KBXXKB.js +0 -0
- package/dist/{storage-types-DtIVejC1.d.ts → storage-types-DsDTCPbp.d.ts} +1 -1
- package/dist/{theo-cloud-3K4DGB3S.js → theo-cloud-RSQCBNXL.js} +4 -4
- package/dist/theo-cloud-RSQCBNXL.js.map +1 -0
- package/dist/upgrade-readiness-GUJBCJIS.js +0 -0
- package/dist/{vercel-MWW24ABC.js → vercel-TKPZK62A.js} +3 -3
- package/dist/vite-plugin/index.d.ts +3 -1
- package/dist/vite-plugin/index.js +20 -8
- package/dist/{vite-plugin-6MPHTKIW.js → vite-plugin-CR4JDFUQ.js} +9 -9
- package/dist/vite-plugin-CR4JDFUQ.js.map +1 -0
- package/package.json +59 -10
- package/LICENSE +0 -201
- package/dist/build-MREW6MVS.js.map +0 -1
- package/dist/chunk-2BQYEBCK.js.map +0 -1
- package/dist/chunk-5OFPQK6N.js.map +0 -1
- package/dist/chunk-5OKZY2VL.js.map +0 -1
- package/dist/chunk-64JI5LTO.js.map +0 -1
- package/dist/chunk-7TOMTMHT.js.map +0 -1
- package/dist/chunk-C52GSJPF.js.map +0 -1
- package/dist/chunk-CZM6WWWS.js +0 -2450
- package/dist/chunk-CZM6WWWS.js.map +0 -1
- package/dist/chunk-KJVEJILQ.js.map +0 -1
- package/dist/chunk-O4BLVPML.js.map +0 -1
- package/dist/chunk-O7335ZGH.js.map +0 -1
- package/dist/chunk-PB4GR7EP.js.map +0 -1
- package/dist/chunk-SNDZQ64K.js.map +0 -1
- package/dist/chunk-TPCT3MXV.js.map +0 -1
- package/dist/chunk-VRHXOKRP.js.map +0 -1
- package/dist/chunk-WZ7CPVQB.js.map +0 -1
- package/dist/chunk-X2OLD264.js.map +0 -1
- package/dist/chunk-X4JAX2U3.js.map +0 -1
- package/dist/chunk-XP7YXRHO.js.map +0 -1
- package/dist/chunk-YLBSSEHX.js.map +0 -1
- package/dist/chunk-ZOXNJV2O.js.map +0 -1
- package/dist/dev-emit-MLDFOLUU.js.map +0 -1
- package/dist/dispatcher-E6QV32BO.js.map +0 -1
- package/dist/dist-E6BL4ZVY.js.map +0 -1
- package/dist/generate-DT4IIAHF.js.map +0 -1
- package/dist/index-li83Swxa.d.ts +0 -506
- package/dist/lib-NL5JXGEB.js.map +0 -1
- package/dist/proper-lockfile-4Y3DP3RP.js.map +0 -1
- package/dist/registry-5QFTLOL2.js +0 -25
- package/dist/schema-CjVly9c_.d.ts +0 -274
- package/dist/sqlite-vec-ARPNUBWT.js.map +0 -1
- package/dist/start-R7XSQL5L.js.map +0 -1
- package/dist/theo-cloud-3K4DGB3S.js.map +0 -1
- /package/dist/{actions-virtual-module-HWNTIEPI.js.map → actions-virtual-module-IY46EEEX.js.map} +0 -0
- /package/dist/{actions-virtual-module-SEIPACG5.js.map → actions-virtual-module-XNHDNCZ6.js.map} +0 -0
- /package/dist/{aws-lambda-XYCGZH3I.js.map → aws-lambda-GKSTX6HD.js.map} +0 -0
- /package/dist/{bun-K73TQEWC.js.map → bun-ISHSNFIO.js.map} +0 -0
- /package/dist/{check-PZR67OY5.js.map → check-NXYPLM6M.js.map} +0 -0
- /package/dist/{dispatcher-AQJGI3HU.js.map → chunk-E3JH6YUM.js.map} +0 -0
- /package/dist/{node-3APTLGWB.js.map → chunk-JHEAWR3L.js.map} +0 -0
- /package/dist/{chunk-ZJW5FFYJ.js.map → chunk-LC5PYNJP.js.map} +0 -0
- /package/dist/{chunk-YWWHK7R6.js.map → chunk-MR7OLIC6.js.map} +0 -0
- /package/dist/{scan-C2BOKLSY.js.map → chunk-NPMCKOX4.js.map} +0 -0
- /package/dist/{chunk-BE2LKDI7.js.map → chunk-WEGALZEU.js.map} +0 -0
- /package/dist/{cloudflare-LCAOTRYZ.js.map → cloudflare-OJGJ7R2D.js.map} +0 -0
- /package/dist/{schema-VR6YNVLQ.js.map → cookies-MJKMGJ7N.js.map} +0 -0
- /package/dist/{deno-deploy-ZN4RE5HF.js.map → deno-deploy-BHWKFTOW.js.map} +0 -0
- /package/dist/{dev-6P2DM33Q.js.map → dev-MJVSRZGF.js.map} +0 -0
- /package/dist/{dev-emit-PRSRPDHA.js.map → dev-emit-HHP2XJXE.js.map} +0 -0
- /package/dist/{vite-plugin-6MPHTKIW.js.map → dispatcher-63LBXYLE.js.map} +0 -0
- /package/dist/{info-FJ4NXKTB.js.map → info-47VB62MV.js.map} +0 -0
- /package/dist/{netlify-5VQ22Z2P.js.map → netlify-GSNSJGMN.js.map} +0 -0
- /package/dist/{openapi-SOKZTFE3.js.map → openapi-NFLSNNVQ.js.map} +0 -0
- /package/dist/{registry-5QFTLOL2.js.map → registry-QHEZ3BWB.js.map} +0 -0
- /package/dist/{routes-XVRAUH6H.js.map → routes-3A4XGIEJ.js.map} +0 -0
- /package/dist/{services-typed-client-XWYYBWGW.js.map → services-typed-client-KK6RHRDG.js.map} +0 -0
- /package/dist/{services-typed-client-ZX5JUHH3.js.map → services-typed-client-T7M5VPBP.js.map} +0 -0
- /package/dist/{static-TZBVBDTB.js.map → static-QI5NQT2X.js.map} +0 -0
- /package/dist/{vercel-MWW24ABC.js.map → vercel-TKPZK62A.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/webhook/timing-safe-equal.ts","../src/server/webhook/raw-body.ts","../src/server/webhook/define-webhook.ts"],"sourcesContent":["/**\n * Constant-time byte comparison.\n *\n * Wraps `node:crypto.timingSafeEqual` when available (Node 6.6+) and falls\n * back to a constant-time XOR loop for runtimes without `node:crypto`\n * (Cloudflare Workers, Deno, Bun edge). Required to defeat timing attacks\n * on webhook signature verification — a naive `===` or `Buffer.equals`\n * would short-circuit on first mismatch byte and leak signature position\n * via wall-clock timing.\n *\n * Web Crypto's `crypto.subtle.verify` would also work, but requires a\n * `CryptoKey` object and is asymmetric (verify a signature against a key).\n * For our case — comparing two pre-computed digests — raw byte equality\n * is the correct primitive.\n *\n * @see https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b\n * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/verify\n */\n\ntype NodeTimingSafeEqual = (a: Uint8Array, b: Uint8Array) => boolean\n\nlet cachedNodeImpl: NodeTimingSafeEqual | null | undefined\n\nfunction resolveNodeImpl(): NodeTimingSafeEqual | null {\n if (cachedNodeImpl !== undefined) return cachedNodeImpl\n try {\n // require() avoids the dynamic-import-of-node-builtin warnings some\n // bundlers emit; `node:crypto` is always synchronously available.\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const cryptoMod = require('node:crypto') as {\n timingSafeEqual?: NodeTimingSafeEqual\n }\n cachedNodeImpl = cryptoMod.timingSafeEqual ?? null\n } catch {\n cachedNodeImpl = null\n }\n return cachedNodeImpl\n}\n\n/**\n * Returns true iff `a` and `b` have identical bytes. Takes time\n * proportional to `a.length` regardless of where bytes differ.\n *\n * @throws TypeError if either argument is not a Uint8Array.\n */\nexport function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array)) {\n throw new TypeError(\n `timingSafeEqual expects Uint8Array arguments (got ${typeof a} and ${typeof b})`,\n )\n }\n // Length mismatch is constant-time-safe by construction (no per-byte work).\n if (a.length !== b.length) return false\n if (a.length === 0) return true\n\n // Prefer the Node built-in when present — it's implemented in C and\n // matches the contract exactly. node:crypto.timingSafeEqual throws on\n // length mismatch, which we've already filtered above.\n const nodeImpl = resolveNodeImpl()\n if (nodeImpl) return nodeImpl(a, b)\n\n // Fallback: XOR-accumulate every byte into one accumulator. NEVER\n // short-circuit. The compiler / V8 will not constant-fold this loop\n // because the inputs are dynamic.\n let diff = 0\n for (let i = 0; i < a.length; i++) {\n diff |= a[i] ^ b[i]\n }\n return diff === 0\n}\n","/**\n * Read a Web `Request` body as a raw string EXACTLY ONCE and expose it\n * alongside the original request (which remains readable by downstream\n * code). Enforces a configurable `maxBodyBytes` cap to prevent OOM via\n * pathological POST.\n *\n * MUST be called FIRST in the webhook pipeline, before any other code\n * touches the body (`request.text()`, `request.json()`, parsers). Once\n * the body is consumed, `Request.clone()` throws and this function fails.\n *\n * EC-101: default `maxBodyBytes = 1_000_000` (1 MB) covers Stripe\n * (256 KB max), Slack (4 MB but compressed), and is well below Node\n * memory thresholds. GitHub webhooks up to 25 MB MUST opt in\n * (`maxBodyBytes: 25_000_000`).\n *\n * @see https://docs.stripe.com/webhooks/signatures\n * @see https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks\n */\n\nexport const DEFAULT_MAX_BODY_BYTES = 1_000_000\n\nexport interface ReadRawBodyOptions {\n /** Maximum bytes to read before throwing `BodyTooLargeError`. Defaults to 1 MB. */\n maxBodyBytes?: number\n}\n\nexport interface RawBodyResult {\n /** UTF-8 decoded body (or empty string when no body present). */\n rawBody: string\n /** The ORIGINAL request, still readable by downstream code. */\n bodyClone: Request\n}\n\n/**\n * Thrown when the request body exceeds `maxBodyBytes`. Carries status\n * 413 (Payload Too Large) and stable code `BODY_TOO_LARGE` so callers\n * can map it to an HTTP response without sniffing the message.\n */\nexport class BodyTooLargeError extends Error {\n readonly status = 413\n readonly code = 'BODY_TOO_LARGE'\n\n constructor(\n public readonly limit: number,\n public readonly actualSeen: number,\n ) {\n super(`Request body exceeds maxBodyBytes: seen ${actualSeen} bytes, limit ${limit} bytes`)\n this.name = 'BodyTooLargeError'\n }\n}\n\nexport async function readRawBody(\n request: Request,\n options: ReadRawBodyOptions = {},\n): Promise<RawBodyResult> {\n const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES\n\n // Clone the request so the original remains readable. `.clone()` throws\n // synchronously (TypeError) if the body was already consumed — we let\n // that propagate so callers fail fast with a clear error.\n const clone = request.clone()\n\n // No body at all (GET, HEAD, or empty POST). Both clone.body and\n // clone.text() handle this gracefully but we short-circuit for clarity\n // and to avoid an unnecessary stream read.\n if (clone.body === null) {\n return { rawBody: '', bodyClone: request }\n }\n\n const reader = clone.body.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\n\n try {\n for (;;) {\n const { value, done } = await reader.read()\n if (done) break\n total += value.byteLength\n if (total > maxBodyBytes) {\n // Cancel the stream without awaiting (avoids deadlock on some\n // ReadableStream impls). The finally block releases the lock.\n void reader.cancel()\n throw new BodyTooLargeError(maxBodyBytes, total)\n }\n chunks.push(value)\n }\n } finally {\n try {\n reader.releaseLock()\n } catch {\n // lock may already be released by cancel()\n }\n }\n\n // Concat all chunks into a single Uint8Array, then UTF-8 decode.\n const bytes = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n bytes.set(chunk, offset)\n offset += chunk.byteLength\n }\n const rawBody = new TextDecoder().decode(bytes)\n\n return { rawBody, bodyClone: request }\n}\n","import {\n extractTraceContext,\n generateNewTraceContext,\n} from '../observability/trace-context-propagation.js'\n\nimport { BodyTooLargeError, readRawBody } from './raw-body.js'\nimport type {\n DefineWebhookOptions,\n VerifyResult,\n WebhookContext,\n WebhookDefinition,\n} from './webhook-types.js'\n\n/**\n * Declare a webhook handler with first-class signature verification\n * (R0.5.10, ADR-0005). Pure identity helper — returns the definition\n * unchanged for downstream dispatch.\n *\n * @example\n * ```ts\n * // server/webhooks/stripe.ts\n * import { defineWebhook } from 'theokit/server'\n * import { stripe } from 'theokit/server/webhook/providers'\n *\n * export default defineWebhook({\n * verify: stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),\n * async handler({ rawBody }) {\n * const event = JSON.parse(rawBody)\n * // ...\n * return new Response('ok')\n * },\n * })\n * ```\n */\nexport function defineWebhook(opts: DefineWebhookOptions): WebhookDefinition {\n return {\n verify: opts.verify,\n handler: opts.handler,\n maxBodyBytes: opts.maxBodyBytes,\n __theokit_kind: 'webhook',\n }\n}\n\n/**\n * Dispatch a webhook request through the definition's verify → handler\n * pipeline. Returns the handler's `Response` (or a wrapped one), or a\n * 401/413 response when verification or body size guards trip.\n *\n * EC-101: enforces `maxBodyBytes` (default 1MB via readRawBody).\n * EC-103: every throw from `verify` (sync or async) is treated as\n * `{ok: false, reason: 'verify threw: <message>'}`. Handler NEVER\n * invoked on verify failure.\n */\nexport async function dispatchWebhook(def: WebhookDefinition, request: Request): Promise<Response> {\n let rawBody: string\n let bodyClone: Request\n try {\n const result = await readRawBody(request, { maxBodyBytes: def.maxBodyBytes })\n rawBody = result.rawBody\n bodyClone = result.bodyClone\n } catch (err) {\n if (err instanceof BodyTooLargeError) {\n return new Response(JSON.stringify({ error: err.code, message: err.message }), {\n status: 413,\n headers: { 'content-type': 'application/json' },\n })\n }\n return new Response(\n JSON.stringify({\n error: 'BAD_REQUEST',\n message: err instanceof Error ? err.message : 'failed to read body',\n }),\n { status: 400, headers: { 'content-type': 'application/json' } },\n )\n }\n\n // Resolve traceId — propagate or generate.\n const incomingTrace = extractTraceContext(request.headers)\n const traceId = incomingTrace?.trace_id ?? generateNewTraceContext().trace_id\n\n // EC-103: verify exception MUST close the door, never let handler run.\n let verifyResult: VerifyResult\n try {\n verifyResult = await def.verify(bodyClone)\n } catch (err) {\n verifyResult = {\n ok: false,\n reason: `verify threw: ${err instanceof Error ? err.message : String(err)}`,\n }\n }\n\n if (!verifyResult.ok) {\n return new Response(\n JSON.stringify({ error: 'WEBHOOK_VERIFY_FAILED', message: verifyResult.reason }),\n { status: 401, headers: { 'content-type': 'application/json' } },\n )\n }\n\n const ctx: WebhookContext = {\n request: bodyClone,\n rawBody,\n traceId,\n signal: request.signal,\n }\n\n const result = await def.handler(ctx)\n if (result instanceof Response) return result\n if (result === undefined || result === null) {\n return new Response(null, { status: 204 })\n }\n return new Response(JSON.stringify(result), {\n status: 200,\n headers: { 'content-type': 'application/json' },\n })\n}\n"],"mappings":"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","names":["result"]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import "tsx/esm";
|
|
3
3
|
import {
|
|
4
4
|
walkSourceFiles
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-COXLEZCN.js";
|
|
6
6
|
|
|
7
7
|
// src/core/contracts/http-methods.ts
|
|
8
8
|
var HTTP_METHODS = [
|
|
@@ -18,7 +18,7 @@ var HTTP_METHOD_LOWERCASE = HTTP_METHODS.map((m) => m.toLowerCase());
|
|
|
18
18
|
|
|
19
19
|
// src/server/scan/scan.ts
|
|
20
20
|
import { existsSync, statSync } from "fs";
|
|
21
|
-
import { extname, join, relative } from "path";
|
|
21
|
+
import { basename, extname, join, relative } from "path";
|
|
22
22
|
|
|
23
23
|
// src/server/scan/detect-http-methods.ts
|
|
24
24
|
import { readFileSync } from "fs";
|
|
@@ -73,6 +73,31 @@ function detectExportedHttpMethods(filePath, content) {
|
|
|
73
73
|
return [...found].sort();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// src/server/scan/errors.ts
|
|
77
|
+
var ROUTER_MIGRATION_GUIDE_URL = "https://theokit.dev/migration/0.3-to-0.4-router";
|
|
78
|
+
var RouterConventionError = class extends Error {
|
|
79
|
+
name = "RouterConventionError";
|
|
80
|
+
file;
|
|
81
|
+
suggestion;
|
|
82
|
+
migrationUrl;
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
const migrationUrl = opts.migrationUrl ?? ROUTER_MIGRATION_GUIDE_URL;
|
|
85
|
+
const message = [
|
|
86
|
+
`Router convention violation: dotted route basename is not supported in theokit 0.4+.`,
|
|
87
|
+
``,
|
|
88
|
+
` File: ${opts.file}`,
|
|
89
|
+
` Use directory-nested form: ${opts.suggestion}`,
|
|
90
|
+
``,
|
|
91
|
+
`Migration guide: ${migrationUrl}`,
|
|
92
|
+
`Run \`theokit migrate router\` to convert all dotted basenames automatically.`
|
|
93
|
+
].join("\n");
|
|
94
|
+
super(message);
|
|
95
|
+
this.file = opts.file;
|
|
96
|
+
this.suggestion = opts.suggestion;
|
|
97
|
+
this.migrationUrl = migrationUrl;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
76
101
|
// src/server/scan/match.ts
|
|
77
102
|
function compilePattern(routePath) {
|
|
78
103
|
const paramNames = [];
|
|
@@ -102,6 +127,61 @@ function matchRoute(url, routes) {
|
|
|
102
127
|
|
|
103
128
|
// src/server/scan/scan.ts
|
|
104
129
|
var ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
130
|
+
var TEST_OR_SPEC_RE = /\.(test|spec)\.[jt]sx?$/;
|
|
131
|
+
function isTestOrSpecFile(filePath) {
|
|
132
|
+
return TEST_OR_SPEC_RE.test(basename(filePath));
|
|
133
|
+
}
|
|
134
|
+
function hasDotOutsideBrackets(segment) {
|
|
135
|
+
let depth = 0;
|
|
136
|
+
for (const ch of segment) {
|
|
137
|
+
if (ch === "[") depth++;
|
|
138
|
+
else if (ch === "]") depth--;
|
|
139
|
+
else if (ch === "." && depth === 0) return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
function splitDottedSegmentOutsideBrackets(segment) {
|
|
144
|
+
const parts = [];
|
|
145
|
+
let current = "";
|
|
146
|
+
let depth = 0;
|
|
147
|
+
for (const ch of segment) {
|
|
148
|
+
if (ch === "[") {
|
|
149
|
+
depth++;
|
|
150
|
+
current += ch;
|
|
151
|
+
} else if (ch === "]") {
|
|
152
|
+
depth--;
|
|
153
|
+
current += ch;
|
|
154
|
+
} else if (ch === "." && depth === 0) {
|
|
155
|
+
if (current) parts.push(current);
|
|
156
|
+
current = "";
|
|
157
|
+
} else {
|
|
158
|
+
current += ch;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (current) parts.push(current);
|
|
162
|
+
return parts;
|
|
163
|
+
}
|
|
164
|
+
function buildDirectoryNestedSuggestion(filePath, routesDir) {
|
|
165
|
+
const rel = relative(routesDir, filePath).replace(/\\/g, "/");
|
|
166
|
+
const ext = extname(rel);
|
|
167
|
+
const withoutExt = rel.slice(0, -ext.length);
|
|
168
|
+
const segments = withoutExt.split("/").flatMap(splitDottedSegmentOutsideBrackets);
|
|
169
|
+
return `routes/${segments.join("/")}${ext}`;
|
|
170
|
+
}
|
|
171
|
+
function assertNoDottedSegment(filePath, routesDir) {
|
|
172
|
+
const rel = relative(routesDir, filePath).replace(/\\/g, "/");
|
|
173
|
+
const ext = extname(rel);
|
|
174
|
+
const withoutExt = rel.slice(0, -ext.length);
|
|
175
|
+
const segments = withoutExt.split("/");
|
|
176
|
+
for (const seg of segments) {
|
|
177
|
+
if (hasDotOutsideBrackets(seg)) {
|
|
178
|
+
throw new RouterConventionError({
|
|
179
|
+
file: filePath,
|
|
180
|
+
suggestion: buildDirectoryNestedSuggestion(filePath, routesDir)
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
105
185
|
function fileToRoutePath(filePath, routesDir) {
|
|
106
186
|
let rel = relative(routesDir, filePath);
|
|
107
187
|
const ext = extname(rel);
|
|
@@ -123,6 +203,8 @@ function scanServerRoutes(serverDir) {
|
|
|
123
203
|
}
|
|
124
204
|
const results = [];
|
|
125
205
|
walkSourceFiles(routesDir, { extensions: ROUTE_EXTENSIONS }, (absPath) => {
|
|
206
|
+
if (isTestOrSpecFile(absPath)) return;
|
|
207
|
+
assertNoDottedSegment(absPath, routesDir);
|
|
126
208
|
const routePath = fileToRoutePath(absPath, routesDir);
|
|
127
209
|
const { pattern, paramNames } = compilePattern(routePath);
|
|
128
210
|
const methods = detectExportedHttpMethods(absPath);
|
|
@@ -186,4 +268,4 @@ export {
|
|
|
186
268
|
scanServerRoutes,
|
|
187
269
|
scanWebSocketRoutes
|
|
188
270
|
};
|
|
189
|
-
//# sourceMappingURL=chunk-
|
|
271
|
+
//# sourceMappingURL=chunk-VBZCVFSA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/contracts/http-methods.ts","../src/server/scan/scan.ts","../src/server/scan/detect-http-methods.ts","../src/server/scan/errors.ts","../src/server/scan/match.ts","../src/server/scan/ws-scan.ts"],"sourcesContent":["/**\n * Canonical HTTP method set used by the typed client codegen + Proxy runtime.\n * Kept here (under `server/scan/`) because the SCAN is the first consumer —\n * `vite-plugin/app-typed-client.ts` (Phase 2) and `client/app-client.ts` (Phase 3)\n * import from here to ensure a single source of truth.\n *\n * Lowercase variants are derived via `.toLowerCase()` at call sites; we do not\n * export them separately to avoid drift between the two lists.\n */\n\nexport const HTTP_METHODS = [\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'HEAD',\n 'OPTIONS',\n] as const\n\nexport type HttpMethod = (typeof HTTP_METHODS)[number]\n\nexport const HTTP_METHOD_LOWERCASE = HTTP_METHODS.map((m) => m.toLowerCase()) as readonly string[]\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks `serverDir/routes/` derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { existsSync, statSync } from 'node:fs'\nimport { basename, extname, join, relative } from 'node:path'\n\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nimport { detectExportedHttpMethods } from './detect-http-methods.js'\nimport { RouterConventionError } from './errors.js'\nimport { compilePattern, type ServerRouteNode } from './match.js'\n\nconst ROUTE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])\n\n// EC-4: Co-located unit/spec tests must be silently skipped by the scanner,\n// BEFORE the dotted-basename check fires. Matches `*.test.ts|tsx|js|jsx`\n// and `*.spec.ts|tsx|js|jsx`.\nconst TEST_OR_SPEC_RE = /\\.(test|spec)\\.[jt]sx?$/\n\nfunction isTestOrSpecFile(filePath: string): boolean {\n return TEST_OR_SPEC_RE.test(basename(filePath))\n}\n\nfunction hasDotOutsideBrackets(segment: string): boolean {\n let depth = 0\n for (const ch of segment) {\n if (ch === '[') depth++\n else if (ch === ']') depth--\n else if (ch === '.' && depth === 0) return true\n }\n return false\n}\n\nfunction splitDottedSegmentOutsideBrackets(segment: string): string[] {\n const parts: string[] = []\n let current = ''\n let depth = 0\n for (const ch of segment) {\n if (ch === '[') {\n depth++\n current += ch\n } else if (ch === ']') {\n depth--\n current += ch\n } else if (ch === '.' && depth === 0) {\n if (current) parts.push(current)\n current = ''\n } else {\n current += ch\n }\n }\n if (current) parts.push(current)\n return parts\n}\n\nfunction buildDirectoryNestedSuggestion(filePath: string, routesDir: string): string {\n const rel = relative(routesDir, filePath).replace(/\\\\/g, '/')\n const ext = extname(rel)\n const withoutExt = rel.slice(0, -ext.length)\n const segments = withoutExt.split('/').flatMap(splitDottedSegmentOutsideBrackets)\n return `routes/${segments.join('/')}${ext}`\n}\n\nfunction assertNoDottedSegment(filePath: string, routesDir: string): void {\n const rel = relative(routesDir, filePath).replace(/\\\\/g, '/')\n const ext = extname(rel)\n const withoutExt = rel.slice(0, -ext.length)\n const segments = withoutExt.split('/')\n for (const seg of segments) {\n if (hasDotOutsideBrackets(seg)) {\n throw new RouterConventionError({\n file: filePath,\n suggestion: buildDirectoryNestedSuggestion(filePath, routesDir),\n })\n }\n }\n}\n\nfunction fileToRoutePath(filePath: string, routesDir: string): string {\n let rel = relative(routesDir, filePath)\n // Strip extension\n const ext = extname(rel)\n rel = rel.slice(0, -ext.length)\n // Normalize separators\n rel = rel.replace(/\\\\/g, '/')\n // Strip index suffix\n if (rel.endsWith('/index')) {\n rel = rel.slice(0, -6)\n } else if (rel === 'index') {\n rel = ''\n }\n // Replace [...param] with :...param (catch-all, before regular params).\n // Replace [param] with :param. Inputs are file paths bounded by the\n // OS filename limit; the bracket capture is bounded by `]`.\n rel = rel.replace(/\\[\\.\\.\\.([^\\]]+)\\]/g, ':...$1')\n // eslint-disable-next-line sonarjs/slow-regex -- bounded by `]`; input is a single filename\n rel = rel.replace(/\\[([^\\]]+)\\]/g, ':$1')\n return `/api/${rel}`\n}\n\nexport function scanServerRoutes(serverDir: string): ServerRouteNode[] {\n const routesDir = join(serverDir, 'routes')\n if (!existsSync(routesDir) || !statSync(routesDir).isDirectory()) {\n return []\n }\n\n const results: ServerRouteNode[] = []\n walkSourceFiles(routesDir, { extensions: ROUTE_EXTENSIONS }, (absPath) => {\n // EC-4: skip co-located test/spec files BEFORE the dotted-basename check\n if (isTestOrSpecFile(absPath)) return\n\n // G6 T1.1: reject dotted basenames (legacy convention that produced wrong\n // paramNames due to greedy `:(?:\\.\\.\\.)?([^/]+)` regex in compilePattern).\n assertNoDottedSegment(absPath, routesDir)\n\n const routePath = fileToRoutePath(absPath, routesDir)\n const { pattern, paramNames } = compilePattern(routePath)\n const methods = detectExportedHttpMethods(absPath)\n results.push({\n filePath: absPath,\n routePath,\n paramNames,\n pattern,\n methods,\n })\n })\n\n // T1.4 / EC-2 — refuse to scan if a user route collides with the reserved\n // batch endpoint path. User must rename or disable batching.\n const conflicting = results.find((r) => r.routePath === '/api/__theo_batch__')\n if (conflicting) {\n throw new Error(\n `Server route ${conflicting.filePath} resolves to '/api/__theo_batch__' which is reserved for the batch endpoint. Rename the route or disable batching in theo.config.ts.`,\n )\n }\n\n // Sort: static first, then dynamic, then catch-all last\n const isCatchAll = (route: ServerRouteNode) => route.routePath.includes(':...')\n results.sort((a, b) => {\n const aStatic = a.paramNames.length === 0\n const bStatic = b.paramNames.length === 0\n const aCatchAll = isCatchAll(a)\n const bCatchAll = isCatchAll(b)\n\n // Static routes first\n if (aStatic && !bStatic) return -1\n if (!aStatic && bStatic) return 1\n // Catch-all routes last\n if (aCatchAll && !bCatchAll) return 1\n if (!aCatchAll && bCatchAll) return -1\n return a.routePath.localeCompare(b.routePath)\n })\n\n return results\n}\n","/**\n * Detect which HTTP-method named exports a route file declares.\n *\n * Uses the TypeScript compiler API (not regex) per G1 edge-case review EC-4:\n * regex over file content emits false positives for `// export const GET = ...`\n * in comments and `` `export const GET = ...` `` in template literals. AST\n * walking avoids both classes of bug.\n *\n * `typescript` ships as CommonJS with internal dynamic `require('fs')`.\n * When loaded via ESM `import`, the dynamic requires fail at module-bootstrap.\n * We use `createRequire(import.meta.url)` to keep the package on its native\n * CJS path. The type-only namespace import gives us the AST helpers shape.\n *\n * Returns the set of HTTP methods (uppercase) the file exports. Empty array\n * means the file has no HTTP exports (the route file is util-only).\n */\n\nimport { readFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\n\nimport type * as TS from 'typescript'\n\nimport { HTTP_METHODS, type HttpMethod } from '../../core/contracts/http-methods.js'\n\nconst require_ = createRequire(import.meta.url)\n// eslint-disable-next-line @typescript-eslint/no-require-imports\nconst ts = require_('typescript') as typeof TS\n\nconst HTTP_METHOD_NAMES = new Set<string>(HTTP_METHODS)\n\nfunction hasExportModifier(modifiers: readonly TS.Modifier[] | undefined): boolean {\n if (!modifiers) return false\n for (const m of modifiers) {\n if (m.kind === ts.SyntaxKind.ExportKeyword) return true\n }\n return false\n}\n\nfunction collectFromStatement(stmt: TS.Statement, found: Set<HttpMethod>): void {\n // `export const GET = ...` / `export function GET ...` / `export async function GET ...`\n if (ts.isVariableStatement(stmt) && hasExportModifier(ts.getModifiers(stmt))) {\n for (const decl of stmt.declarationList.declarations) {\n if (ts.isIdentifier(decl.name) && HTTP_METHOD_NAMES.has(decl.name.text)) {\n found.add(decl.name.text as HttpMethod)\n }\n }\n return\n }\n\n if (\n (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) &&\n hasExportModifier(ts.getModifiers(stmt))\n ) {\n if (stmt.name && HTTP_METHOD_NAMES.has(stmt.name.text)) {\n found.add(stmt.name.text as HttpMethod)\n }\n return\n }\n\n // `export { GET }` / `export { handler as GET } from './shared'` (EC-5)\n if (ts.isExportDeclaration(stmt) && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {\n for (const spec of stmt.exportClause.elements) {\n // spec.name is the exported (re-)name; spec.propertyName is the original (when renamed)\n if (HTTP_METHOD_NAMES.has(spec.name.text)) {\n found.add(spec.name.text as HttpMethod)\n }\n }\n }\n}\n\nexport function detectExportedHttpMethods(filePath: string, content?: string): HttpMethod[] {\n const src = content ?? readFileSync(filePath, 'utf-8')\n const sourceFile = ts.createSourceFile(\n filePath,\n src,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ false,\n ts.ScriptKind.TS,\n )\n const found = new Set<HttpMethod>()\n for (const stmt of sourceFile.statements) {\n collectFromStatement(stmt, found)\n }\n return [...found].sort()\n}\n","/**\n * Router-convention errors thrown by the server-route scanner.\n *\n * Plan: .claude/knowledge-base/plans/g6-router-convention-plan.md v1.1\n *\n * theokit 0.4.0+ enforces directory-nested file-system routing\n * (`auth/[provider]/login.ts`) and REJECTS dotted-basename routes\n * (`auth.[provider].login.ts`) because the legacy regex extracted the\n * dotted basename incorrectly — `params.provider` was undefined at request\n * time. See ADR-XXX (router-convention-decision) in CHANGELOG 0.4.0.\n */\n\n/**\n * Canonical migration guide URL. T4.2 establishes this as the authoritative\n * landing page. EC-3: error message uses this constant so the URL never\n * drifts from the doc location.\n */\nexport const ROUTER_MIGRATION_GUIDE_URL = 'https://theokit.dev/migration/0.3-to-0.4-router'\n\nexport interface RouterConventionErrorOptions {\n /** Absolute path of the offending route file. */\n file: string\n /** Suggested directory-nested replacement path (relative, e.g. `routes/auth/[provider]/login.ts`). */\n suggestion: string\n /** Migration guide URL (defaults to `ROUTER_MIGRATION_GUIDE_URL`). */\n migrationUrl?: string\n}\n\n/**\n * Thrown by `scanServerRoutes` when a route file uses the legacy\n * dotted-basename convention (`auth.[provider].login.ts`).\n *\n * The error is FAIL-FAST by design — running with a route that has wrong\n * `paramNames` produces silent 404s at request time, which is strictly\n * worse than a build-time error.\n */\nexport class RouterConventionError extends Error {\n override readonly name = 'RouterConventionError'\n readonly file: string\n readonly suggestion: string\n readonly migrationUrl: string\n\n constructor(opts: RouterConventionErrorOptions) {\n const migrationUrl = opts.migrationUrl ?? ROUTER_MIGRATION_GUIDE_URL\n const message = [\n `Router convention violation: dotted route basename is not supported in theokit 0.4+.`,\n ``,\n ` File: ${opts.file}`,\n ` Use directory-nested form: ${opts.suggestion}`,\n ``,\n `Migration guide: ${migrationUrl}`,\n `Run \\`theokit migrate router\\` to convert all dotted basenames automatically.`,\n ].join('\\n')\n super(message)\n this.file = opts.file\n this.suggestion = opts.suggestion\n this.migrationUrl = migrationUrl\n }\n}\n","export interface ServerRouteNode {\n filePath: string\n routePath: string\n paramNames: string[]\n pattern: RegExp\n /** HTTP methods (uppercase) the route file exports. Optional for backward\n * compatibility with manifests generated before G1. Empty array means the\n * file has no HTTP exports (util-only); undefined means \"not detected\". */\n methods?: string[]\n}\n\nexport function compilePattern(routePath: string): {\n pattern: RegExp\n paramNames: string[]\n} {\n const paramNames: string[] = []\n // Single pass: handle both catch-all (:...name) and regular (:name) params\n const regexStr = routePath.replace(/:(?:\\.\\.\\.)?([^/]+)/g, (match: string, name: string) => {\n paramNames.push(name)\n // Catch-all matches across slashes, regular matches single segment\n return match.startsWith(':...') ? '(.+)' : '([^/]+)'\n })\n // `regexStr` is derived from a developer-authored route path (build-time\n // input, not HTTP-controlled). The `security/detect-non-literal-regexp`\n // rule cannot see this constraint — disable narrowly.\n // eslint-disable-next-line security/detect-non-literal-regexp -- route pattern from build-time scan, never HTTP input\n return { pattern: new RegExp(`^${regexStr}$`), paramNames }\n}\n\nexport function matchRoute(\n url: string,\n routes: ServerRouteNode[],\n): { route: ServerRouteNode; params: Record<string, string> } | null {\n // Strip query string and trailing slash\n let path = url.split('?')[0]\n if (path.length > 1 && path.endsWith('/')) {\n path = path.slice(0, -1)\n }\n\n for (const route of routes) {\n const match = route.pattern.exec(path)\n if (match) {\n const params: Record<string, string> = {}\n route.paramNames.forEach((name, i) => {\n params[name] = match[i + 1]\n })\n return { route, params }\n }\n }\n return null\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks `serverDir/ws/` derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { existsSync, statSync } from 'node:fs'\nimport { extname, join, relative } from 'node:path'\n\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nconst WS_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])\n\nexport interface WebSocketRouteNode {\n filePath: string\n wsPath: string\n}\n\nexport function scanWebSocketRoutes(serverDir: string): WebSocketRouteNode[] {\n const wsDir = join(serverDir, 'ws')\n if (!existsSync(wsDir) || !statSync(wsDir).isDirectory()) {\n return []\n }\n\n const results: WebSocketRouteNode[] = []\n walkSourceFiles(wsDir, { extensions: WS_EXTENSIONS }, (absPath) => {\n let rel = relative(wsDir, absPath)\n rel = rel.replace(/\\\\/g, '/')\n rel = rel.slice(0, -extname(rel).length)\n if (rel.endsWith('/index')) rel = rel.slice(0, -6)\n else if (rel === 'index') rel = ''\n results.push({\n filePath: absPath,\n wsPath: `/ws/${rel}`,\n })\n })\n return results\n}\n"],"mappings":"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","names":["existsSync","statSync","extname","join","relative","join","existsSync","statSync","relative","extname"]}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/server/rate-limit/rate-limit-store.ts
|
|
2
|
+
var InMemoryStore = class _InMemoryStore {
|
|
3
|
+
store = /* @__PURE__ */ new Map();
|
|
4
|
+
/** Bound the map to prevent unbounded growth from pathological inputs. */
|
|
5
|
+
static MAX_ENTRIES = 1e5;
|
|
6
|
+
/** GC sweep interval, milliseconds. */
|
|
7
|
+
static GC_INTERVAL_MS = 3e4;
|
|
8
|
+
gcTimer = null;
|
|
9
|
+
constructor() {
|
|
10
|
+
if (typeof setInterval !== "undefined") {
|
|
11
|
+
const timer = setInterval(() => {
|
|
12
|
+
this.sweepExpired();
|
|
13
|
+
}, _InMemoryStore.GC_INTERVAL_MS);
|
|
14
|
+
this.gcTimer = timer;
|
|
15
|
+
const maybeUnref = timer.unref;
|
|
16
|
+
if (typeof maybeUnref === "function") maybeUnref.call(timer);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Synchronous fast-path used by the legacy sync `createRateLimiter`
|
|
21
|
+
* surface (`api-middleware.ts` is sync). The async `incr` delegates to
|
|
22
|
+
* this for in-memory; external adapters override `incr` directly.
|
|
23
|
+
*/
|
|
24
|
+
incrSync(key, windowMs) {
|
|
25
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`InMemoryStore.incr: windowMs must be a positive finite number (got ${windowMs})`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
if (this.store.size >= _InMemoryStore.MAX_ENTRIES) {
|
|
32
|
+
const first = this.store.keys().next().value;
|
|
33
|
+
if (first !== void 0) this.store.delete(first);
|
|
34
|
+
}
|
|
35
|
+
const entry = this.store.get(key);
|
|
36
|
+
if (!entry || now >= entry.resetAt) {
|
|
37
|
+
const fresh = { count: 1, resetAt: now + windowMs };
|
|
38
|
+
this.store.set(key, fresh);
|
|
39
|
+
return { ...fresh };
|
|
40
|
+
}
|
|
41
|
+
entry.count++;
|
|
42
|
+
return { count: entry.count, resetAt: entry.resetAt };
|
|
43
|
+
}
|
|
44
|
+
/** Sweep expired entries. Called by the GC timer; safe to call manually. */
|
|
45
|
+
sweepExpired() {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
for (const [k, v] of this.store) {
|
|
48
|
+
if (v.resetAt <= now) this.store.delete(k);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Stop the GC timer. Call from tests or when discarding the store. After
|
|
53
|
+
* `dispose`, the store still works but no longer auto-sweeps.
|
|
54
|
+
*/
|
|
55
|
+
dispose() {
|
|
56
|
+
if (this.gcTimer) {
|
|
57
|
+
clearInterval(this.gcTimer);
|
|
58
|
+
this.gcTimer = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// The async surface is required by the `RateLimitStore` interface
|
|
62
|
+
// (Redis/etc. adapters are inherently async). For the in-memory case
|
|
63
|
+
// we just adapt the sync implementations to Promises.
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return
|
|
65
|
+
async incr(key, windowMs) {
|
|
66
|
+
return this.incrSync(key, windowMs);
|
|
67
|
+
}
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return
|
|
69
|
+
async get(key) {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const entry = this.store.get(key);
|
|
72
|
+
if (!entry) return null;
|
|
73
|
+
if (now >= entry.resetAt) return null;
|
|
74
|
+
return { count: entry.count, resetAt: entry.resetAt };
|
|
75
|
+
}
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return
|
|
77
|
+
async reset(key) {
|
|
78
|
+
this.store.delete(key);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/server/rate-limit/rate-limit.ts
|
|
83
|
+
function createRateLimiter(config, opts = {}) {
|
|
84
|
+
const store = opts.store ?? new InMemoryStore();
|
|
85
|
+
const isInMemory = store instanceof InMemoryStore;
|
|
86
|
+
return function checkRateLimit(req) {
|
|
87
|
+
const key = req.socket?.remoteAddress ?? "unknown";
|
|
88
|
+
if (isInMemory) {
|
|
89
|
+
const state = store.incrSync(key, config.windowMs);
|
|
90
|
+
return resultFromState(state, config);
|
|
91
|
+
}
|
|
92
|
+
throw new Error(
|
|
93
|
+
"createRateLimiter: async RateLimitStore implementations are not supported by this sync fa\xE7ade. Use the InMemoryStore default or build a custom middleware around the async store directly."
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function resultFromState(state, config) {
|
|
98
|
+
if (state.count > config.max) {
|
|
99
|
+
const retryAfter = Math.ceil((state.resetAt - Date.now()) / 1e3);
|
|
100
|
+
return {
|
|
101
|
+
limited: true,
|
|
102
|
+
headers: {
|
|
103
|
+
"X-RateLimit-Limit": String(config.max),
|
|
104
|
+
"X-RateLimit-Remaining": "0",
|
|
105
|
+
"Retry-After": String(retryAfter)
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
limited: false,
|
|
111
|
+
headers: {
|
|
112
|
+
"X-RateLimit-Limit": String(config.max),
|
|
113
|
+
"X-RateLimit-Remaining": String(Math.max(0, config.max - state.count))
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function createRateLimiterWeb(config, opts = {}) {
|
|
118
|
+
const store = opts.store ?? new InMemoryStore();
|
|
119
|
+
const isInMemory = store instanceof InMemoryStore;
|
|
120
|
+
return function checkRateLimitWeb(clientIp) {
|
|
121
|
+
const key = clientIp.length > 0 ? clientIp : "unknown";
|
|
122
|
+
if (isInMemory) {
|
|
123
|
+
const state = store.incrSync(key, config.windowMs);
|
|
124
|
+
return resultFromState(state, config);
|
|
125
|
+
}
|
|
126
|
+
throw new Error(
|
|
127
|
+
"createRateLimiterWeb: async RateLimitStore implementations are not supported by this sync fa\xE7ade. Use the InMemoryStore default or build a custom middleware around the async store directly."
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
InMemoryStore,
|
|
134
|
+
createRateLimiter,
|
|
135
|
+
createRateLimiterWeb
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=chunk-VMEWD57H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/rate-limit/rate-limit-store.ts","../src/server/rate-limit/rate-limit.ts"],"sourcesContent":["/**\n * T2.1 — RateLimitStore interface.\n *\n * Pluggable backend for the rate limiter. The default `InMemoryStore`\n * preserves current single-instance behavior. Multi-instance deployments\n * (TheoCloud canary, K8s replicas) opt in to a distributed adapter\n * (Redis, Cloudflare KV) without bloating single-instance apps.\n *\n * Contract per ADR D1:\n * - `incr` is atomic — concurrent calls for the same key both observe\n * the same `resetAt` and increment count by 1.\n * - `get` returns `null` for expired entries (not just absent — checks\n * `now > resetAt`).\n * - `reset` removes the key entirely; next `incr` creates fresh.\n *\n * Async signature is honest about Redis adapters even though in-memory\n * implementation is synchronous. Callers MUST await.\n */\n\nexport interface RateLimitState {\n /** Number of requests counted in the current window. */\n count: number\n /** Absolute timestamp (ms since epoch) when this window expires. */\n resetAt: number\n}\n\nexport interface RateLimitStore {\n /**\n * Atomic increment-and-get. If the key is missing OR the previous\n * window expired (`now >= resetAt`), create with `count=1, resetAt=now+windowMs`.\n * Otherwise increment count by 1, preserving the original resetAt.\n */\n incr(key: string, windowMs: number): Promise<RateLimitState>\n\n /** Read current state. Returns `null` for missing or expired entries. */\n get(key: string): Promise<RateLimitState | null>\n\n /** Remove a key. Used by login throttling on success (T6.1). */\n reset(key: string): Promise<void>\n}\n\n/**\n * Default in-memory store. Backed by `Map<string, RateLimitState>`.\n *\n * GC: every 1000 `incr` calls, expired entries are removed. Pathological\n * key explosion is bounded by `MAX_ENTRIES` (LRU-evict oldest insertion).\n *\n * Single-thread by virtue of Node's event loop — `incr` is atomic at the\n * JavaScript level (no preemption between Map.get and Map.set).\n */\nexport class InMemoryStore implements RateLimitStore {\n private store = new Map<string, RateLimitState>()\n /** Bound the map to prevent unbounded growth from pathological inputs. */\n static readonly MAX_ENTRIES = 100_000\n /** GC sweep interval, milliseconds. */\n static readonly GC_INTERVAL_MS = 30_000\n\n private gcTimer: ReturnType<typeof setInterval> | null = null\n\n constructor() {\n // CR-007 fix: GC runs on a timer OUTSIDE the request hot path.\n // Pre-fix, every 1000th `incrSync` triggered a synchronous Map sweep\n // of up to 100K entries — an attacker with diverse client IPs could\n // keep the Map full and force a blocking sweep predictably.\n if (typeof setInterval !== 'undefined') {\n const timer = setInterval(() => {\n this.sweepExpired()\n }, InMemoryStore.GC_INTERVAL_MS)\n this.gcTimer = timer\n // Allow the process to exit even with the timer pending. `unref` is\n // Node-only; not present on browser/Cloudflare `setInterval`.\n const maybeUnref = (timer as { unref?: () => void }).unref\n if (typeof maybeUnref === 'function') maybeUnref.call(timer)\n }\n }\n\n /**\n * Synchronous fast-path used by the legacy sync `createRateLimiter`\n * surface (`api-middleware.ts` is sync). The async `incr` delegates to\n * this for in-memory; external adapters override `incr` directly.\n */\n incrSync(key: string, windowMs: number): RateLimitState {\n if (!Number.isFinite(windowMs) || windowMs <= 0) {\n throw new Error(\n `InMemoryStore.incr: windowMs must be a positive finite number (got ${windowMs})`,\n )\n }\n const now = Date.now()\n\n // Bounded LRU-ish: when over cap, drop oldest insertion.\n if (this.store.size >= InMemoryStore.MAX_ENTRIES) {\n const first = this.store.keys().next().value\n if (first !== undefined) this.store.delete(first)\n }\n\n const entry = this.store.get(key)\n if (!entry || now >= entry.resetAt) {\n const fresh = { count: 1, resetAt: now + windowMs }\n this.store.set(key, fresh)\n return { ...fresh }\n }\n entry.count++\n return { count: entry.count, resetAt: entry.resetAt }\n }\n\n /** Sweep expired entries. Called by the GC timer; safe to call manually. */\n sweepExpired(): void {\n const now = Date.now()\n for (const [k, v] of this.store) {\n if (v.resetAt <= now) this.store.delete(k)\n }\n }\n\n /**\n * Stop the GC timer. Call from tests or when discarding the store. After\n * `dispose`, the store still works but no longer auto-sweeps.\n */\n dispose(): void {\n if (this.gcTimer) {\n clearInterval(this.gcTimer)\n this.gcTimer = null\n }\n }\n\n // The async surface is required by the `RateLimitStore` interface\n // (Redis/etc. adapters are inherently async). For the in-memory case\n // we just adapt the sync implementations to Promises.\n // eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return\n async incr(key: string, windowMs: number): Promise<RateLimitState> {\n return this.incrSync(key, windowMs)\n }\n\n // eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return\n async get(key: string): Promise<RateLimitState | null> {\n const now = Date.now()\n const entry = this.store.get(key)\n if (!entry) return null\n if (now >= entry.resetAt) return null\n return { count: entry.count, resetAt: entry.resetAt }\n }\n\n // eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return\n async reset(key: string): Promise<void> {\n this.store.delete(key)\n }\n}\n","import type { IncomingMessage } from 'node:http'\n\nimport { InMemoryStore, type RateLimitStore } from './rate-limit-store.js'\n\n/**\n * Rate limit configuration — basic single-bucket shape. Per ADR D2, the\n * per-route + per-user variant is layered on top via T2.2; this base\n * struct is the smallest unit.\n */\nexport interface RateLimitConfig {\n windowMs: number\n max: number\n}\n\nexport interface RateLimitResult {\n limited: boolean\n headers: Record<string, string>\n}\n\n/**\n * Create a rate limiter that consumes a pluggable `RateLimitStore`.\n *\n * Backwards-compatible signature: callers passing only `config` get the\n * default `InMemoryStore`. Distributed deployments pass a Redis adapter\n * (or any other `RateLimitStore` implementation) via `opts.store`.\n *\n * T2.1: synchronous return for back-compat with the current\n * `api-middleware.ts` integration point. The async store contract is\n * exercised lazily — we use a sync fast-path against the in-memory store\n * (single-thread JS makes this safe). External stores remain async and\n * would require a different async wrapper at the middleware layer.\n */\nexport function createRateLimiter(config: RateLimitConfig, opts: { store?: RateLimitStore } = {}) {\n const store = opts.store ?? new InMemoryStore()\n const isInMemory = store instanceof InMemoryStore\n\n return function checkRateLimit(req: IncomingMessage): RateLimitResult {\n // `req.socket` is typed as always-present in Node typings; defensive\n // for test doubles (object literals without `socket`).\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive for test doubles\n const key = req.socket?.remoteAddress ?? 'unknown'\n\n if (isInMemory) {\n // Sync fast-path: the in-memory store exposes a sync incr.\n const state = store.incrSync(key, config.windowMs)\n return resultFromState(state, config)\n }\n\n // External stores are NOT supported via this synchronous façade in\n // 0.3.x. Users plugging Redis adapters today must wire it via a\n // dedicated async middleware path (out-of-scope for T2.1; tracked\n // for the follow-up `@theokit/rate-limit-redis` package).\n throw new Error(\n 'createRateLimiter: async RateLimitStore implementations are not supported by this sync façade. ' +\n 'Use the InMemoryStore default or build a custom middleware around the async store directly.',\n )\n }\n}\n\nfunction resultFromState(\n state: { count: number; resetAt: number },\n config: RateLimitConfig,\n): RateLimitResult {\n if (state.count > config.max) {\n const retryAfter = Math.ceil((state.resetAt - Date.now()) / 1000)\n return {\n limited: true,\n headers: {\n 'X-RateLimit-Limit': String(config.max),\n 'X-RateLimit-Remaining': '0',\n 'Retry-After': String(retryAfter),\n },\n }\n }\n return {\n limited: false,\n headers: {\n 'X-RateLimit-Limit': String(config.max),\n 'X-RateLimit-Remaining': String(Math.max(0, config.max - state.count)),\n },\n }\n}\n\n/**\n * T5a.2 Phase D slice 2/3 — Web-Standards single-bucket rate limiter.\n *\n * Mirror of `createRateLimiter(config, opts)` returning a checker that\n * accepts `(clientIp: string)` instead of `(req: IncomingMessage)`. Web\n * `Request` has no `req.socket.remoteAddress` — the IP is resolved by\n * the caller per-runtime (Node adapter from socket; CF Workers from\n * `cf-connecting-ip`; etc., same convention as Phase D slice 1/3's\n * `DeriveKeyRequestContext.clientIp`).\n *\n * Same `RateLimitConfig`, same `InMemoryStore` default, same async\n * store rejection (use a dedicated async middleware for external stores).\n * Same `RateLimitResult` return shape.\n *\n * **Signature difference from `createRateLimiter`:** this Web factory's\n * checker takes a raw IP string instead of a Request object. The IP is\n * the ONLY input the bucket needs; passing a Request object would force\n * the caller to populate `request.headers.get('x-forwarded-for')` or\n * similar without giving us any safer extraction. KISS — accept the IP\n * directly, document the per-runtime resolution at the adapter boundary.\n */\nexport function createRateLimiterWeb(\n config: RateLimitConfig,\n opts: { store?: RateLimitStore } = {},\n) {\n const store = opts.store ?? new InMemoryStore()\n const isInMemory = store instanceof InMemoryStore\n\n return function checkRateLimitWeb(clientIp: string): RateLimitResult {\n const key = clientIp.length > 0 ? clientIp : 'unknown'\n if (isInMemory) {\n const state = store.incrSync(key, config.windowMs)\n return resultFromState(state, config)\n }\n throw new Error(\n 'createRateLimiterWeb: async RateLimitStore implementations are not supported by this sync façade. ' +\n 'Use the InMemoryStore default or build a custom middleware around the async store directly.',\n )\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
compilePattern,
|
|
5
5
|
scanServerRoutes,
|
|
6
6
|
scanWebSocketRoutes
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-VBZCVFSA.js";
|
|
8
8
|
import {
|
|
9
9
|
scanServerActions
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-COXLEZCN.js";
|
|
11
11
|
|
|
12
12
|
// src/server/scan/manifest.ts
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
@@ -72,4 +72,4 @@ export {
|
|
|
72
72
|
writeManifest,
|
|
73
73
|
loadManifest
|
|
74
74
|
};
|
|
75
|
-
//# sourceMappingURL=chunk-
|
|
75
|
+
//# sourceMappingURL=chunk-WEGALZEU.js.map
|
|
@@ -6,12 +6,7 @@ import { mkdirSync, writeFileSync } from "fs";
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
|
|
8
8
|
// src/vite-plugin/openapi-emit/zod-to-openapi.ts
|
|
9
|
-
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = "ZodToOpenApiError";
|
|
13
|
-
}
|
|
14
|
-
};
|
|
9
|
+
import { z } from "zod";
|
|
15
10
|
function zodToOpenApiSchema(schema, options = {}) {
|
|
16
11
|
const ctx = options.ctx ?? { seen: /* @__PURE__ */ new Map(), components: {} };
|
|
17
12
|
const name = options.name;
|
|
@@ -21,150 +16,52 @@ function zodToOpenApiSchema(schema, options = {}) {
|
|
|
21
16
|
}
|
|
22
17
|
ctx.seen.set(schema, name);
|
|
23
18
|
ctx.components[name] = {};
|
|
24
|
-
const body =
|
|
19
|
+
const body = convertSchema(schema, ctx);
|
|
25
20
|
ctx.components[name] = body;
|
|
26
21
|
return { $ref: `#/components/schemas/${name}` };
|
|
27
22
|
}
|
|
28
|
-
return
|
|
23
|
+
return convertSchema(schema, ctx);
|
|
29
24
|
}
|
|
30
|
-
function
|
|
25
|
+
function convertSchema(schema, ctx) {
|
|
31
26
|
const existing = ctx.seen.get(schema);
|
|
32
27
|
if (existing !== void 0) {
|
|
33
28
|
return { $ref: `#/components/schemas/${existing}` };
|
|
34
29
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
case "ZodNumber":
|
|
41
|
-
return convertNumber(schema);
|
|
42
|
-
case "ZodBoolean":
|
|
43
|
-
return { type: "boolean" };
|
|
44
|
-
case "ZodLiteral":
|
|
45
|
-
return convertLiteral(schema);
|
|
46
|
-
case "ZodEnum":
|
|
47
|
-
return convertEnum(schema);
|
|
48
|
-
case "ZodArray":
|
|
49
|
-
return convertArray(schema, ctx);
|
|
50
|
-
case "ZodObject":
|
|
51
|
-
return convertObject(schema, ctx);
|
|
52
|
-
case "ZodUnion":
|
|
53
|
-
return convertUnion(schema, ctx);
|
|
54
|
-
case "ZodDiscriminatedUnion":
|
|
55
|
-
return convertDiscriminatedUnion(schema, ctx);
|
|
56
|
-
case "ZodOptional":
|
|
57
|
-
return convert(schema._def.innerType, ctx);
|
|
58
|
-
case "ZodNullable": {
|
|
59
|
-
const inner = convert(
|
|
60
|
-
schema._def.innerType,
|
|
61
|
-
ctx
|
|
62
|
-
);
|
|
63
|
-
return { ...inner, nullable: true };
|
|
64
|
-
}
|
|
65
|
-
case "ZodEffects":
|
|
66
|
-
case "ZodTransform":
|
|
67
|
-
return convert(schema._def.schema, ctx);
|
|
68
|
-
case "ZodDefault":
|
|
69
|
-
return convert(schema._def.innerType, ctx);
|
|
70
|
-
case "ZodLazy": {
|
|
71
|
-
const getter = schema._def.getter;
|
|
72
|
-
return convert(getter(), ctx);
|
|
73
|
-
}
|
|
74
|
-
case "ZodRecord": {
|
|
75
|
-
const valueType = schema._def.valueType;
|
|
76
|
-
return { type: "object", additionalProperties: convert(valueType, ctx) };
|
|
77
|
-
}
|
|
78
|
-
case "ZodAny":
|
|
79
|
-
case "ZodUnknown":
|
|
80
|
-
return {};
|
|
81
|
-
// no constraint
|
|
82
|
-
case "ZodNull":
|
|
83
|
-
return { type: "null" };
|
|
84
|
-
case "ZodFunction":
|
|
85
|
-
throw new ZodToOpenApiError(
|
|
86
|
-
"Unsupported Zod type: z.function() cannot be represented in OpenAPI Schema. Remove from the route schema."
|
|
87
|
-
);
|
|
88
|
-
case "ZodPromise":
|
|
89
|
-
throw new ZodToOpenApiError(
|
|
90
|
-
"Unsupported Zod type: z.promise() cannot be represented in OpenAPI Schema. Unwrap the value before passing to defineRoute."
|
|
91
|
-
);
|
|
92
|
-
default:
|
|
93
|
-
throw new ZodToOpenApiError(
|
|
94
|
-
`Unsupported Zod type: ${typeName || "(unknown)"}. Open an issue if this construct should be supported.`
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
function convertString(schema) {
|
|
99
|
-
const def = schema._def;
|
|
100
|
-
const checks = def.checks ?? [];
|
|
101
|
-
const out = { type: "string" };
|
|
102
|
-
for (const c of checks) {
|
|
103
|
-
if (c.kind === "email") out.format = "email";
|
|
104
|
-
else if (c.kind === "uuid") out.format = "uuid";
|
|
105
|
-
else if (c.kind === "url") out.format = "uri";
|
|
106
|
-
else if (c.kind === "datetime") out.format = "date-time";
|
|
107
|
-
}
|
|
108
|
-
return out;
|
|
109
|
-
}
|
|
110
|
-
function convertNumber(schema) {
|
|
111
|
-
const def = schema._def;
|
|
112
|
-
const checks = def.checks ?? [];
|
|
113
|
-
const isInt = checks.some((c) => c.kind === "int");
|
|
114
|
-
return { type: isInt ? "integer" : "number" };
|
|
115
|
-
}
|
|
116
|
-
function convertLiteral(schema) {
|
|
117
|
-
const value = schema._def.value;
|
|
118
|
-
const typed = literalTypeOf(value);
|
|
119
|
-
if (typed === null) {
|
|
120
|
-
return { type: "null", enum: [null] };
|
|
30
|
+
try {
|
|
31
|
+
const jsonSchema = z.toJSONSchema(schema);
|
|
32
|
+
return toOpenApi3(jsonSchema);
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
121
35
|
}
|
|
122
|
-
return { type: typed, enum: [value] };
|
|
123
|
-
}
|
|
124
|
-
function literalTypeOf(value) {
|
|
125
|
-
if (value === null) return null;
|
|
126
|
-
if (typeof value === "string") return "string";
|
|
127
|
-
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
128
|
-
if (typeof value === "boolean") return "boolean";
|
|
129
|
-
return "string";
|
|
130
36
|
}
|
|
131
|
-
function
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
37
|
+
function toOpenApi3(js) {
|
|
38
|
+
const { $schema: _, $defs: _defs, ...rest } = js;
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
41
|
+
if (key === "type" && Array.isArray(value)) {
|
|
42
|
+
const types = value;
|
|
43
|
+
const hasNull = types.includes("null");
|
|
44
|
+
const nonNull = types.filter((t) => t !== "null");
|
|
45
|
+
out.type = nonNull.length === 1 ? nonNull[0] : nonNull;
|
|
46
|
+
if (hasNull) out.nullable = true;
|
|
47
|
+
} else if (key === "properties" && value && typeof value === "object") {
|
|
48
|
+
const props = {};
|
|
49
|
+
for (const [pk, pv] of Object.entries(value)) {
|
|
50
|
+
props[pk] = pv && typeof pv === "object" ? toOpenApi3(pv) : pv;
|
|
51
|
+
}
|
|
52
|
+
out.properties = props;
|
|
53
|
+
} else if (key === "items" && value && typeof value === "object") {
|
|
54
|
+
out.items = toOpenApi3(value);
|
|
55
|
+
} else if ((key === "anyOf" || key === "oneOf" || key === "allOf") && Array.isArray(value)) {
|
|
56
|
+
out[key] = value.map((v) => toOpenApi3(v));
|
|
57
|
+
} else if (key === "additionalProperties" && value && typeof value === "object") {
|
|
58
|
+
out.additionalProperties = toOpenApi3(value);
|
|
59
|
+
} else {
|
|
60
|
+
out[key] = value;
|
|
61
|
+
}
|
|
148
62
|
}
|
|
149
|
-
const out = {
|
|
150
|
-
type: "object",
|
|
151
|
-
properties,
|
|
152
|
-
additionalProperties: false
|
|
153
|
-
};
|
|
154
|
-
if (required.length > 0) out.required = required;
|
|
155
63
|
return out;
|
|
156
64
|
}
|
|
157
|
-
function convertUnion(schema, ctx) {
|
|
158
|
-
const options = schema._def.options;
|
|
159
|
-
return { oneOf: options.map((o) => convert(o, ctx)) };
|
|
160
|
-
}
|
|
161
|
-
function convertDiscriminatedUnion(schema, ctx) {
|
|
162
|
-
const def = schema._def;
|
|
163
|
-
return {
|
|
164
|
-
oneOf: def.options.map((o) => convert(o, ctx)),
|
|
165
|
-
discriminator: { propertyName: def.discriminator }
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
65
|
|
|
169
66
|
// src/vite-plugin/openapi-emit/emit.ts
|
|
170
67
|
var METHOD_TO_KEY = {
|
|
@@ -355,4 +252,4 @@ export {
|
|
|
355
252
|
emitOpenApi,
|
|
356
253
|
loadRoutesForOpenApi
|
|
357
254
|
};
|
|
358
|
-
//# sourceMappingURL=chunk-
|
|
255
|
+
//# sourceMappingURL=chunk-WGHJD6I7.js.map
|