├── .changeset ├── README.md └── config.json ├── .github ├── ISSUE_TEMPLATE │ ├── 🐛-bug-report.md │ └── 💡-feature-request.md └── workflows │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── elysia-rate-limit.iml ├── modules.xml └── vcs.xml ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── biome.json ├── bun.lock ├── example ├── basic.ts ├── multiInstance.ts └── multiInstanceInjected.ts ├── package.json ├── src ├── @types │ ├── Context.ts │ ├── Generator.ts │ ├── Options.ts │ └── Server.ts ├── constants │ ├── defaultOptions.spec.ts │ └── defaultOptions.ts ├── index.ts └── services │ ├── defaultContext.spec.ts │ ├── defaultContext.ts │ ├── defaultKeyGenerator.spec.ts │ ├── defaultKeyGenerator.ts │ ├── logger.ts │ ├── plugin.spec.ts │ └── plugin.ts ├── tsconfig.json └── tsdown.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🐛-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'type: bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Before open the issue** 11 | - [ ] I tried remove `node_modules/`, `bun.lockb`, and do a clean installation. 12 | - [ ] I tried update Bun, and Elysia to latest version. 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | Please provide a small reproduction project if possible. Also if possible, please run your app with `DEBUG=elysia-rate-limit:*` and provide the logs. 19 | 20 | Steps to reproduce the behavior: 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | **Expected behavior** 27 | A clear and concise description of what you expected to happen. 28 | 29 | **Environments** 30 | 1. Bun version: 31 | 2. Host Operating system: 32 | 3. Running on Docker?: 33 | 34 | **Screenshots** 35 | If applicable, add screenshots to help explain your problem. 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/💡-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature request" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'type: feature-request' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR Checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v4 15 | - name: bun 16 | uses: oven-sh/setup-bun@v2 17 | with: 18 | bun-version: latest 19 | - name: install 20 | run: bun i 21 | - name: test 22 | run: bun test 23 | 24 | build: 25 | name: Build 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: checkout 29 | uses: actions/checkout@v4 30 | - name: bun 31 | uses: oven-sh/setup-bun@v2 32 | with: 33 | bun-version: latest 34 | - name: install 35 | run: bun i 36 | - name: build 37 | run: bun run build 38 | - name: publish 39 | run: bunx pkg-pr-new publish 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | concurrency: ${{ github.workflow }}-${{ github.ref }} 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | contents: write 15 | packages: write 16 | pull-requests: write 17 | issues: read 18 | steps: 19 | - name: checkout 20 | uses: actions/checkout@v4 21 | - name: bun 22 | uses: oven-sh/setup-bun@v2 23 | with: 24 | bun-version: latest 25 | - name: install 26 | run: bun i 27 | - name: test 28 | run: bun test 29 | - name: build 30 | run: bun run build 31 | - name: changesets 32 | id: changesets 33 | uses: changesets/action@v1 34 | with: 35 | publish: bun changeset publish 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | NPM_CONFIG_PROVENANCE: true 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Node ### 38 | # Logs 39 | logs 40 | *.log 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | lerna-debug.log* 45 | .pnpm-debug.log* 46 | 47 | # Diagnostic reports (https://nodejs.org/api/report.html) 48 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Directory for instrumented libs generated by jscoverage/JSCover 57 | lib-cov 58 | 59 | # Coverage directory used by tools like istanbul 60 | coverage 61 | *.lcov 62 | 63 | # nyc test coverage 64 | .nyc_output 65 | 66 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 67 | .grunt 68 | 69 | # Bower dependency directory (https://bower.io/) 70 | bower_components 71 | 72 | # node-waf configuration 73 | .lock-wscript 74 | 75 | # Compiled binary addons (https://nodejs.org/api/addons.html) 76 | build/Release 77 | 78 | # Dependency directories 79 | node_modules/ 80 | jspm_packages/ 81 | 82 | # Snowpack dependency directory (https://snowpack.dev/) 83 | web_modules/ 84 | 85 | # TypeScript cache 86 | *.tsbuildinfo 87 | 88 | # Optional npm cache directory 89 | .npm 90 | 91 | # Optional eslint cache 92 | .eslintcache 93 | 94 | # Optional stylelint cache 95 | .stylelintcache 96 | 97 | # Microbundle cache 98 | .rpt2_cache/ 99 | .rts2_cache_cjs/ 100 | .rts2_cache_es/ 101 | .rts2_cache_umd/ 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variable files 113 | .env 114 | .env.development.local 115 | .env.test.local 116 | .env.production.local 117 | .env.local 118 | 119 | # parcel-bundler cache (https://parceljs.org/) 120 | .cache 121 | .parcel-cache 122 | 123 | # Next.js build output 124 | .next 125 | out 126 | 127 | # Nuxt.js build / generate output 128 | .nuxt 129 | dist 130 | 131 | # Gatsby files 132 | .cache/ 133 | # Comment in the public line in if your project uses Gatsby and not Next.js 134 | # https://nextjs.org/blog/next-9-1#public-directory-support 135 | # public 136 | 137 | # vuepress build output 138 | .vuepress/dist 139 | 140 | # vuepress v2.x temp and cache directory 141 | .temp 142 | 143 | # Docusaurus cache and generated files 144 | .docusaurus 145 | 146 | # Serverless directories 147 | .serverless/ 148 | 149 | # FuseBox cache 150 | .fusebox/ 151 | 152 | # DynamoDB Local files 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | .tern-port 157 | 158 | # Stores VSCode versions used for testing VSCode extensions 159 | .vscode-test 160 | 161 | # yarn v2 162 | .yarn/cache 163 | .yarn/unplugged 164 | .yarn/build-state.yml 165 | .yarn/install-state.gz 166 | .pnp.* 167 | 168 | ### Node Patch ### 169 | # Serverless Webpack directories 170 | .webpack/ 171 | 172 | # Optional stylelint cache 173 | 174 | # SvelteKit build / generate output 175 | .svelte-kit 176 | 177 | # End of https://www.toptal.com/developers/gitignore/api/node,macos 178 | 179 | #lock file 180 | package-lock.json 181 | pnpm-lock.yaml 182 | yaml.lock 183 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Project exclude paths 5 | /copilot/chatSessions/ 6 | # Editor-based HTTP Client requests 7 | /httpRequests/ 8 | # GitHub Copilot persisted chat sessions 9 | /copilot/chatSessions 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 72 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/elysia-rate-limit.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | bun.lockb 4 | 5 | node_modules 6 | tsconfig.json 7 | pnpm-lock.yaml 8 | jest.config.js 9 | nodemon.json 10 | 11 | example 12 | tests 13 | test 14 | .eslintrc.js 15 | tsconfig.cjs.json 16 | tsconfig.esm.json 17 | 18 | .changeset 19 | .github 20 | .idea 21 | docs 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # elysia-rate-limit 2 | 3 | ## 4.4.0 4 | 5 | ### Minor Changes 6 | 7 | - ab26fc3: change package to esm-only, move build toolchain to tsdown 8 | 9 | ### Patch Changes 10 | 11 | - 41680f1: Use named functions instead of anonymous arrows for better tracing 12 | 13 | ## 4.3.0 14 | 15 | ### Minor Changes 16 | 17 | - 9de6373: change default context to use `@alloc/quick-lru` instead of `lru-cache` 18 | 19 | ### Patch Changes 20 | 21 | - c807a06: reimplement generator to reject edge-case early 22 | 23 | ## 4.2.1 24 | 25 | ### Patch Changes 26 | 27 | - b978900: seeding logic complexity reduction 28 | - b978900: fix plugin duplication due to missing seed provided to Elysia instances 29 | 30 | ## 4.2.1-beta.1 31 | 32 | ### Patch Changes 33 | 34 | - seeding logic complexity reduction 35 | 36 | ## 4.2.1-beta.0 37 | 38 | ### Patch Changes 39 | 40 | - fix plugin duplication due to missing seed provided to Elysia instances 41 | 42 | ## 4.2.0 43 | 44 | ### Minor Changes 45 | 46 | - c19f554: change build pipeline to serve builds from both tsup, and tsc 47 | 48 | ### Patch Changes 49 | 50 | - c19f554: only package `dist/` directory on publish 51 | - c19f554: implement unit testing in repository 52 | 53 | ## 4.2.0-beta.1 54 | 55 | ### Patch Changes 56 | 57 | - only package `dist/` directory on publish 58 | - 4f857b1: implement unit testing in repository 59 | 60 | ## 4.2.0-beta.0 61 | 62 | ### Minor Changes 63 | 64 | - change build pipeline to serve builds from both tsup, and tsc 65 | 66 | ## 4.1.1-beta.0 67 | 68 | ### Patch Changes 69 | 70 | - fix plugin duplication due to missing seed provided to Elysia instances 71 | 72 | ## 4.1.0 73 | 74 | ### Minor Changes 75 | 76 | - 3d0c0ae: added ability to let pligin not to send RateLimit-\* headers 77 | - e867896: scoping `local` is considered unstable, please move to `scoped` 78 | 79 | ### Patch Changes 80 | 81 | - e867896: fixes multiple instances running duplicate jobs at the same time 82 | 83 | ## 4.0.0 84 | 85 | ### Major Changes 86 | 87 | - 17f10e3: **BREAKING CHANGES** remove `responseCode`, and `responseMessage` in favor of new `errorResponse` option. please consult with documentation for more details 88 | 89 | ### Minor Changes 90 | 91 | - 17f10e3: added `injectServer` option 92 | 93 | ## 3.2.2 94 | 95 | ### Patch Changes 96 | 97 | - d4d7a62: added debug logs 98 | - 5e83844: fix context is being shared across multiple local scope 99 | - 5e83844: add example for multiple instance scoping 100 | 101 | ## 3.2.1 102 | 103 | ### Patch Changes 104 | 105 | - 2c5d035: fix generic value of generator types 106 | 107 | ## 3.2.0 108 | 109 | ### Minor Changes 110 | 111 | - ebe2c77: generators function now passed derive values as a thrid argruments 112 | - 55f0e22: allowing user to change plugin hooks scoping behavior 113 | 114 | ## 3.2.0-beta.1 115 | 116 | ### Minor Changes 117 | 118 | - 37dc931: allowing user to change plugin hooks scoping behavior 119 | 120 | ## 3.2.0-beta.0 121 | 122 | ### Minor Changes 123 | 124 | - generators function now passed derive values as a thrid argruments 125 | 126 | ## 3.1.4 127 | 128 | ### Patch Changes 129 | 130 | - 9ffb155: plugin no longer server to be initialized 131 | 132 | ## 3.1.3 133 | 134 | ### Patch Changes 135 | 136 | - 7317819: nextReset time for default context always returns to year 1970. my bad 137 | 138 | ## 3.1.2 139 | 140 | ### Patch Changes 141 | 142 | - dea390c: default generator throws detailed warning message with reason 143 | 144 | ## 3.1.1 145 | 146 | ### Patch Changes 147 | 148 | - ca7f124: `getNextResetTime` implementation are now moved into `defaultContext` 149 | - 28316db: excluding unintended files that being published to npm 150 | - 1bcda4f: `generator` function now accepts `server` option as non-nullable. the plugin also checks if elysia server is initialized first. 151 | - d5056ec: file structure changes to match directory convention 152 | 153 | ## 3.1.0 154 | 155 | ### Minor Changes 156 | 157 | - 8779402: new default context strategy 158 | 159 | ## 3.1.0-beta.0 160 | 161 | ### Minor Changes 162 | 163 | - 8619ac1: new default context strategy 164 | 165 | ## 3.0.1 166 | 167 | ### Patch Changes 168 | 169 | - 17047d9: refactor code structure 170 | 171 | ## 3.0.0 172 | 173 | ### Major Changes 174 | 175 | - 693125a: **Breaking change** Plugin compatibility with [Elysia 1.0](elysiajs.com/blog/elysia-10.html). Please refer to compatibility list in README for older Elysia, Bun versions. 176 | 177 | ## 2.2.0 178 | 179 | ### Minor Changes 180 | 181 | - b666627: Add second parameter to skip with the key so requests can be skipped based on their key 182 | 183 | ## 2.1.0 184 | 185 | ### Minor Changes 186 | 187 | - 0f53f96: reset the clock when attempting to manually reset the context ([#10](https://github.com/rayriffy/elysia-rate-limit/pull/10)) 188 | 189 | ## 2.0.1 190 | 191 | ### Patch Changes 192 | 193 | - d84ebbd: responseMessage type has been changed to `any`, so you can actually return response as anything (i.e. object) 194 | 195 | ## 2.0.0 196 | 197 | ### Major Changes 198 | 199 | - fc6e385: `generator` now determine IP address natively via [`server.requestIP()` function](https://github.com/oven-sh/bun/pull/6165). This is a breaking change for those who use Bun version 1.0.3 or below. Please update your code to support Bun version 1.0.4 or above. 200 | 201 | ## 1.3.0 202 | 203 | ### Minor Changes 204 | 205 | - a5a0b02: bump minimum peer dependency verison of elysia to 0.7.15 206 | 207 | ## 1.2.0 208 | 209 | ### Minor Changes 210 | 211 | - 89d308c: securely signed package with provenance 212 | 213 | ## 1.1.0 214 | 215 | ### Minor Changes 216 | 217 | - dba19a3: minimum version of support bun is now 0.3.0 218 | 219 | ### Patch Changes 220 | 221 | - a34655c: automatic package publishing with changesets 222 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Phumrapee Limpianchop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elysia Rate Limit 2 | 3 | Lightweight rate limiter plugin for [Elysia.js](https://elysiajs.com/) 4 | 5 | [![NPM Version](https://img.shields.io/npm/v/elysia-rate-limit)](https://www.npmjs.com/package/elysia-rate-limit) 6 | [![NPM Downloads](https://img.shields.io/npm/dw/elysia-rate-limit)](https://www.npmjs.com/package/elysia-rate-limit) 7 | [![NPM License](https://img.shields.io/npm/l/elysia-rate-limit)](https://www.npmjs.com/package/elysia-rate-limit) 8 | 9 | ## Install 10 | 11 | ``` 12 | bun add elysia-rate-limit 13 | ``` 14 | 15 | If you're using Bun v1.0.3 or lower, `elysia-rate-limit` v2.0.0 or higher will not be compatible. Please use `elysia-rate-limit` [v1.3.0](https://github.com/rayriffy/elysia-rate-limit/releases/tag/v1.3.0) instead. 16 | 17 | ## Compatibility 18 | 19 | As long as you're on the latest version of Bun, and Elysia. 20 | Using the latest version of `elysia-rate-limit` would works just fine. 21 | However, please refer to the following table to determine which version to use. 22 | 23 | | Plugin version | Requirements | 24 | |----------------|------------------------------| 25 | | 3.0.0+ | Bun > 1.0.3, Elysia >= 1.0.0 | 26 | | 2.0.0 - 2.2.0 | Bun > 1.0.3, Elysia < 1.0.0 | 27 | | 1.0.2 - 1.3.0 | Bun <= 1.0.3, Elysia < 1.0.0 | 28 | 29 | ## Usage 30 | 31 | Check out full sample at [`example`](example/basic.ts) 32 | 33 | ```ts 34 | import { Elysia } from 'elysia' 35 | import { rateLimit } from 'elysia-rate-limit' 36 | 37 | new Elysia().use(rateLimit()).listen(3000) 38 | ``` 39 | 40 | ## Configuration 41 | 42 | ### duration 43 | 44 | `number` 45 | 46 | Default: `60000` 47 | 48 | Duration for requests to be remembered in **milliseconds**. 49 | Also used in the `Retry-After` header when the limit is reached. 50 | 51 | ### max 52 | 53 | `number` 54 | 55 | Default: `10` 56 | 57 | Maximum of request to be allowed during 1 `duration` timeframe. 58 | 59 | ### errorResponse 60 | 61 | `string | Response | Error` 62 | 63 | Default: `rate-limit reached` 64 | 65 | Response to be sent when the rate limit is reached. 66 | 67 | If you define a value as a string, 68 | then it will be sent as a plain text response with status code 429. If you define a value as a `Response` object, 69 | then it will be sent as is. 70 | And if you define a value as an `Error` object, then it will be thrown as an error. 71 | 72 |
73 | Example for Response object response 74 | 75 | ```ts 76 | new Elysia() 77 | .use( 78 | rateLimit({ 79 | errorResponse: new Response("rate-limited", { 80 | status: 429, 81 | headers: new Headers({ 82 | 'Content-Type': 'text/plain', 83 | 'Custom-Header': 'custom', 84 | }), 85 | }), 86 | }) 87 | ) 88 | ``` 89 |
90 | 91 |
92 | Example for Error object response 93 | 94 | ```ts 95 | import { HttpStatusEnum } from 'elysia-http-status-code/status' 96 | 97 | export class RateLimitError extends Error { 98 | constructor( 99 | public message: string = 'rate-limited', 100 | public detail: string = '', 101 | public status: number = HttpStatusEnum.HTTP_429_TOO_MANY_REQUESTS // or just 429 102 | ) { 103 | super(message) 104 | } 105 | } 106 | 107 | new Elysia() 108 | .use( 109 | rateLimit({ 110 | errorResponse: new RateLimitError(), 111 | }) 112 | ) 113 | // use with error hanlder 114 | .error({ 115 | rateLimited: RateLimitError, 116 | }) 117 | .onError({ as: 'global' }, ({ code }) => { 118 | switch (code) { 119 | case 'rateLimited': 120 | return code 121 | break 122 | } 123 | }) 124 | ``` 125 | 126 |
127 | 128 | ### scoping 129 | 130 | `'global' | 'scoped'` 131 | 132 | Default: `'global'` 133 | 134 | Sometimes you may want 135 | to only apply rate limit plugin to curtain Elysia instance. 136 | This option will allow you 137 | to choose scope `local` apply to only current instance and descendant only. 138 | But by default, 139 | rate limit plugin will apply to all instances that apply the plugin. 140 | 141 | Read more : [Scope - ElysiaJS | ElysiaJS](https://elysiajs.com/essential/plugin.html#scope-level) 142 | 143 | ### generator 144 | 145 | `(equest: Request, server: Server | null, derived: T) => MaybePromise` 146 | 147 | Custom key generator to categorize client requests, return as a string. By default, this plugin will categorize client by their IP address via [`server.requestIP()` function](https://github.com/oven-sh/bun/pull/6165). 148 | 149 | If you deploy your server behind a proxy (i.e. NGINX, Cloudflare), you may need to implement your own generator to get client's real IP address. 150 | 151 | ```js 152 | // IMPORTANT: Only use this if your server is behind Cloudflare AND 153 | // you've restricted access to only Cloudflare IPs 154 | const cloudflareGenerator = (req, server) => { 155 | // Verify the request is coming from Cloudflare 156 | // In production, you should maintain a list of Cloudflare IP ranges 157 | // and verify the request IP is in that range 158 | const isFromCloudflare = verifyCloudflareIP(server?.requestIP(req)?.address) 159 | 160 | if (isFromCloudflare) { 161 | // Only trust CF-Connecting-IP if the request comes from Cloudflare 162 | return req.headers.get('CF-Connecting-IP') ?? server?.requestIP(req)?.address ?? '' 163 | } 164 | 165 | // For non-Cloudflare requests, use the direct IP 166 | return server?.requestIP(req)?.address ?? '' 167 | } 168 | 169 | // Example function to verify Cloudflare IPs (implement this based on your needs) 170 | function verifyCloudflareIP(ip) { 171 | // In a real implementation, check if IP is in Cloudflare's IP ranges 172 | // https://www.cloudflare.com/ips/ 173 | return true // Replace with actual implementation 174 | } 175 | ``` 176 | 177 | There's a third argument 178 | where you can use derive values from external plugin within key generator as well. 179 | Only downsize is you have to definitely those types be yourself, 180 | please be sure to test those values before actually defining types manually. 181 | 182 | ```ts 183 | import { ip } from 'elysia-ip' 184 | 185 | import type { SocketAddress } from 'bun' 186 | import type { Generator } from 'elysia-rate-limit' 187 | 188 | const ipGenerator: Generator<{ ip: SocketAddress }> = (_req, _serv, { ip }) => { 189 | return ip 190 | } 191 | ``` 192 | 193 | ### countFailedRequest 194 | 195 | `boolean` 196 | 197 | Default: `false` 198 | 199 | Should this plugin count rate-limit to user when request failed? 200 | By default, 201 | this plugin will refund request count to a client 202 | when `onError` lifecycle called. 203 | ([Learn more in Lifecycle](https://elysiajs.com/concept/middleware.html#life-cycle)) 204 | 205 | ### context 206 | 207 | `Context` 208 | 209 | Context for storing requests count for each client, if you want to implement your own `Context` you can write it to comply with [`Context`](./src/@types/Context.ts) protocol 210 | 211 | ```ts 212 | import type { Context } from 'elysia-rate-limit' 213 | 214 | export class CustomContext implements Context { 215 | // implementation here 216 | } 217 | ``` 218 | 219 | By default, context implementation, caching will be an LRU cache with a maximum of 5,000 entries. If you prefer to use this cache implementation but with larger cache size, you can define a new context with preferred cache size as follows 220 | 221 | ```ts 222 | import { DefaultContext } from 'elysia-rate-limit' 223 | 224 | new Elysia().use( 225 | rateLimit({ 226 | // define max cache size to 10,000 227 | context: new DefaultContext(10_000), 228 | }) 229 | ) 230 | ``` 231 | 232 | ### headers 233 | 234 | `boolean` 235 | 236 | Default `true` 237 | 238 | Should this plugin automatically set `RateLimit-*` headers to the response? 239 | If you want to disable this feature, you can set this option to `false`. 240 | 241 | ### skip 242 | 243 | `(request: Request, key: string): boolean | Promise` 244 | 245 | Default: `(): false` 246 | 247 | A custom function 248 | to determine that should this request be counted into rate-limit 249 | or not based on information given by `Request` object 250 | (i.e., Skip counting rate-limit on some route) and the key of the given request, 251 | by default, this will always return `false` which means counted everything. 252 | 253 | ### injectServer 254 | 255 | `() => Server` 256 | 257 | Default: `undefined` 258 | 259 | A function to inject server instance to the plugin, 260 | this is useful 261 | when you want to use default key generator in detached Elysia instances. 262 | You can check out the example [here](./example/multiInstanceInjected.ts). 263 | 264 | Please use this function as a last resort, 265 | as defining this option will make plugin to make an extra function call, 266 | which may affect performance. 267 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, 4 | "files": { "ignoreUnknown": false, "ignore": ["**/dist/**"] }, 5 | "formatter": { 6 | "enabled": true, 7 | "useEditorconfig": true, 8 | "formatWithErrors": false, 9 | "indentStyle": "space", 10 | "indentWidth": 2, 11 | "lineEnding": "lf", 12 | "lineWidth": 80, 13 | "attributePosition": "auto", 14 | "bracketSpacing": true 15 | }, 16 | "organizeImports": { "enabled": true }, 17 | "linter": { "enabled": true, "rules": { "recommended": true } }, 18 | "javascript": { 19 | "formatter": { 20 | "jsxQuoteStyle": "double", 21 | "quoteProperties": "asNeeded", 22 | "trailingCommas": "es5", 23 | "semicolons": "asNeeded", 24 | "arrowParentheses": "asNeeded", 25 | "bracketSameLine": false, 26 | "quoteStyle": "single", 27 | "attributePosition": "auto", 28 | "bracketSpacing": true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "elysia-rate-limit", 6 | "dependencies": { 7 | "@alloc/quick-lru": "5.2.0", 8 | "debug": "4.3.4", 9 | }, 10 | "devDependencies": { 11 | "@biomejs/biome": "1.9.4", 12 | "@changesets/cli": "2.28.1", 13 | "@elysiajs/swagger": "1.2.2", 14 | "@tsconfig/node22": "22.0.2", 15 | "@types/bun": "1.2.4", 16 | "@types/debug": "4.1.12", 17 | "elysia": "1.2.23", 18 | "elysia-ip": "1.0.8", 19 | "tsdown": "0.12.3", 20 | "typescript": "5.7.3", 21 | }, 22 | "peerDependencies": { 23 | "elysia": ">= 1.0.0", 24 | }, 25 | }, 26 | }, 27 | "packages": { 28 | "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], 29 | 30 | "@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="], 31 | 32 | "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], 33 | 34 | "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], 35 | 36 | "@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="], 37 | 38 | "@babel/runtime": ["@babel/runtime@7.26.9", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg=="], 39 | 40 | "@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], 41 | 42 | "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], 43 | 44 | "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], 45 | 46 | "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], 47 | 48 | "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], 49 | 50 | "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], 51 | 52 | "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], 53 | 54 | "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], 55 | 56 | "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], 57 | 58 | "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], 59 | 60 | "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.0.10", "", { "dependencies": { "@changesets/config": "^3.1.1", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.2", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-wNyeIJ3yDsVspYvHnEz1xQDq18D9ifed3lI+wxRQRK4pArUcuHgCTrHv0QRnnwjhVCQACxZ+CBih3wgOct6UXw=="], 61 | 62 | "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.6", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-Frkj8hWJ1FRZiY3kzVCKzS0N5mMwWKwmv9vpam7vt8rZjLL1JMthdh6pSDVSPumHPshTTkKZ0VtNbE0cJHZZUg=="], 63 | 64 | "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], 65 | 66 | "@changesets/cli": ["@changesets/cli@2.28.1", "", { "dependencies": { "@changesets/apply-release-plan": "^7.0.10", "@changesets/assemble-release-plan": "^6.0.6", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.1", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/get-release-plan": "^4.0.8", "@changesets/git": "^3.0.2", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "ci-info": "^3.7.0", "enquirer": "^2.4.1", "external-editor": "^3.1.0", "fs-extra": "^7.0.1", "mri": "^1.2.0", "p-limit": "^2.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-PiIyGRmSc6JddQJe/W1hRPjiN4VrMvb2VfQ6Uydy2punBioQrsxppyG5WafinKcW1mT0jOe/wU4k9Zy5ff21AA=="], 67 | 68 | "@changesets/config": ["@changesets/config@3.1.1", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/logger": "^0.1.1", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA=="], 69 | 70 | "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], 71 | 72 | "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="], 73 | 74 | "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.8", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.6", "@changesets/config": "^3.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.3", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-MM4mq2+DQU1ZT7nqxnpveDMTkMBLnwNX44cX7NSxlXmr7f8hO6/S2MXNiXG54uf/0nYnefv0cfy4Czf/ZL/EKQ=="], 75 | 76 | "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], 77 | 78 | "@changesets/git": ["@changesets/git@3.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-r1/Kju9Y8OxRRdvna+nxpQIsMsRQn9dhhAZt94FLDeu0Hij2hnOozW8iqnHBgvu+KdnJppCveQwK4odwfw/aWQ=="], 79 | 80 | "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], 81 | 82 | "@changesets/parse": ["@changesets/parse@0.4.1", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^3.13.1" } }, "sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q=="], 83 | 84 | "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], 85 | 86 | "@changesets/read": ["@changesets/read@0.6.3", "", { "dependencies": { "@changesets/git": "^3.0.2", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.1", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-9H4p/OuJ3jXEUTjaVGdQEhBdqoT2cO5Ts95JTFsQyawmKzpL8FnIeJSyhTDPW1MBRDnwZlHFEM9SpPwJDY5wIg=="], 87 | 88 | "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], 89 | 90 | "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], 91 | 92 | "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], 93 | 94 | "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], 95 | 96 | "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], 97 | 98 | "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], 99 | 100 | "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], 101 | 102 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], 103 | 104 | "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 105 | 106 | "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], 107 | 108 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], 109 | 110 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], 111 | 112 | "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], 113 | 114 | "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], 115 | 116 | "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="], 117 | 118 | "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], 119 | 120 | "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], 121 | 122 | "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], 123 | 124 | "@oxc-project/runtime": ["@oxc-project/runtime@0.71.0", "", {}, "sha512-QwoF5WUXIGFQ+hSxWEib4U/aeLoiDN9JlP18MnBgx9LLPRDfn1iICtcow7Jgey6HLH4XFceWXQD5WBJ39dyJcw=="], 125 | 126 | "@oxc-project/types": ["@oxc-project/types@0.71.0", "", {}, "sha512-5CwQ4MI+P4MQbjLWXgNurA+igGwu/opNetIE13LBs9+V93R64MLvDKOOLZIXSzEfovU3Zef3q3GjPnMTgJTn2w=="], 127 | 128 | "@quansync/fs": ["@quansync/fs@0.1.3", "", { "dependencies": { "quansync": "^0.2.10" } }, "sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg=="], 129 | 130 | "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Mp0/gqiPdepHjjVm7e0yL1acWvI0rJVVFQEADSezvAjon9sjQ7CEg9JnXICD4B1YrPmN9qV/e7cQZCp87tTV4w=="], 131 | 132 | "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "darwin", "cpu": "x64" }, "sha512-40re4rMNrsi57oavRzIOpRGmg3QRlW6Ea8Q3znaqgOuJuKVrrm2bIQInTfkZJG7a4/5YMX7T951d0+toGLTdCA=="], 133 | 134 | "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8BDM939bbMariZupiHp3OmP5N+LXPT4mULA0hZjDaq970PCxv4krZOSMG+HkWUUwmuQROtV+/00xw39EO0P+8g=="], 135 | 136 | "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm" }, "sha512-sntsPaPgrECpBB/+2xrQzVUt0r493TMPI+4kWRMhvMsmrxOqH1Ep5lM0Wua/ZdbfZNwm1aVa5pcESQfNfM4Fhw=="], 137 | 138 | "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5clBW/I+er9F2uM1OFjJFWX86y7Lcy0M+NqsN4s3o07W+8467Zk8oQa4B45vdaXoNUF/yqIAgKkA/OEdQDxZqA=="], 139 | 140 | "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm64" }, "sha512-wv+rnAfQDk9p/CheX8/Kmqk2o1WaFa4xhWI9gOyDMk/ljvOX0u0ubeM8nI1Qfox7Tnh71eV5AjzSePXUhFOyOg=="], 141 | 142 | "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "x64" }, "sha512-gxD0/xhU4Py47IH3bKZbWtvB99tMkUPGPJFRfSc5UB9Osoje0l0j1PPbxpUtXIELurYCqwLBKXIMTQGifox1BQ=="], 143 | 144 | "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "x64" }, "sha512-HotuVe3XUjDwqqEMbm3o3IRkP9gdm8raY/btd/6KE3JGLF/cv4+3ff1l6nOhAZI8wulWDPEXPtE7v+HQEaTXnA=="], 145 | 146 | "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.4" }, "cpu": "none" }, "sha512-8Cx+ucbd8n2dIr21FqBh6rUvTVL0uTgEtKR7l+MUZ5BgY4dFh1e4mPVX8oqmoYwOxBiXrsD2JIOCz4AyKLKxWA=="], 147 | 148 | "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Vhq5vikrVDxAa75fxsyqj0c0Y/uti/TwshXI71Xb8IeUQJOBnmLUsn5dgYf5ljpYYkNa0z9BPAvUDIDMmyDi+w=="], 149 | 150 | "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "ia32" }, "sha512-lN7RIg9Iugn08zP2aZN9y/MIdG8iOOCE93M1UrFlrxMTqPf8X+fDzmR/OKhTSd1A2pYNipZHjyTcb5H8kyQSow=="], 151 | 152 | "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "x64" }, "sha512-7/7cLIn48Y+EpQ4CePvf8reFl63F15yPUlg4ZAhl+RXJIfydkdak1WD8Ir3AwAO+bJBXzrfNL+XQbxm0mcQZmw=="], 153 | 154 | "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5", "", {}, "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ=="], 155 | 156 | "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], 157 | 158 | "@scalar/themes": ["@scalar/themes@0.9.68", "", { "dependencies": { "@scalar/types": "0.0.34" } }, "sha512-466ac2fdQJOBBSLkGUf88vuZVF+qNMeVpjb0aAHrKkxhpjucTPKdTYO8r2dsX1R5k9A13gWPnm594VW5G/bGHw=="], 159 | 160 | "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], 161 | 162 | "@sinclair/typebox": ["@sinclair/typebox@0.34.28", "", {}, "sha512-e2B9vmvaa5ym5hWgCHw5CstP54au6AOLXrhZErLsOyyRzuWJtXl/8TszKtc5x8rw/b+oY7HKS9m9iRI53RK0WQ=="], 163 | 164 | "@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="], 165 | 166 | "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], 167 | 168 | "@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="], 169 | 170 | "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], 171 | 172 | "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], 173 | 174 | "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], 175 | 176 | "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], 177 | 178 | "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], 179 | 180 | "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], 181 | 182 | "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 183 | 184 | "ansis": ["ansis@4.0.0", "", {}, "sha512-P8nrHI1EyW9OfBt1X7hMSwGN2vwRuqHSKJAT1gbLWZRzDa24oHjYwGHvEgHeBepupzk878yS/HBZ0NMPYtbolw=="], 185 | 186 | "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], 187 | 188 | "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], 189 | 190 | "ast-kit": ["ast-kit@2.0.0", "", { "dependencies": { "@babel/parser": "^7.27.2", "pathe": "^2.0.3" } }, "sha512-P63jzlYNz96MF9mCcprU+a7I5/ZQ5QAn3y+mZcPWEcGV3CHF/GWnkFPj3oCrWLUjL47+PD9PNiCUdXxw0cWdsg=="], 191 | 192 | "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], 193 | 194 | "birpc": ["birpc@2.3.0", "", {}, "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g=="], 195 | 196 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 197 | 198 | "bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="], 199 | 200 | "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], 201 | 202 | "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], 203 | 204 | "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], 205 | 206 | "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], 207 | 208 | "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], 209 | 210 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 211 | 212 | "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], 213 | 214 | "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], 215 | 216 | "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], 217 | 218 | "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], 219 | 220 | "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], 221 | 222 | "dts-resolver": ["dts-resolver@2.0.1", "", { "peerDependencies": { "oxc-resolver": "^9.0.2" }, "optionalPeers": ["oxc-resolver"] }, "sha512-Pe2kqaQTNVxleYpt9Q9658fn6rEpoZbMbDpEBbcU6pnuGM3Q0IdM+Rv67kN6qcyp8Bv2Uv9NYy5Y1rG1LSgfoQ=="], 223 | 224 | "elysia": ["elysia@1.2.23", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-hMezhUkbpzZQduu01tmODsNJXk5CJ8oAQvc5gN1+GLv8cjiOFOnMQdEpTtaMplTKU0lr7MBtwL2duVFXEBPKOg=="], 225 | 226 | "elysia-ip": ["elysia-ip@1.0.8", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-xzuBMCN+hoM7BTzjB4QDqY6dxTYaK+NrxnVTilNsKW0+T5KTTWBi3vaj3hdRFw06KLNJIlAKR16yU00lPT5IXw=="], 227 | 228 | "empathic": ["empathic@1.1.0", "", {}, "sha512-rsPft6CK3eHtrlp9Y5ALBb+hfK+DWnA4WFebbazxjWyx8vSm3rZeoM3z9irsjcqO3PYRzlfv27XIB4tz2DV7RA=="], 229 | 230 | "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], 231 | 232 | "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], 233 | 234 | "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], 235 | 236 | "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], 237 | 238 | "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], 239 | 240 | "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], 241 | 242 | "fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="], 243 | 244 | "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 245 | 246 | "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], 247 | 248 | "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], 249 | 250 | "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], 251 | 252 | "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 253 | 254 | "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], 255 | 256 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 257 | 258 | "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], 259 | 260 | "human-id": ["human-id@4.1.1", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg=="], 261 | 262 | "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 263 | 264 | "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 265 | 266 | "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 267 | 268 | "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 269 | 270 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], 271 | 272 | "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], 273 | 274 | "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], 275 | 276 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 277 | 278 | "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], 279 | 280 | "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], 281 | 282 | "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], 283 | 284 | "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], 285 | 286 | "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], 287 | 288 | "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], 289 | 290 | "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], 291 | 292 | "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], 293 | 294 | "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], 295 | 296 | "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], 297 | 298 | "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], 299 | 300 | "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], 301 | 302 | "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], 303 | 304 | "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], 305 | 306 | "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], 307 | 308 | "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], 309 | 310 | "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], 311 | 312 | "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], 313 | 314 | "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], 315 | 316 | "package-manager-detector": ["package-manager-detector@0.2.10", "", { "dependencies": { "quansync": "^0.2.2" } }, "sha512-1wlNZK7HW+UE3eGCcMv3hDaYokhspuIeH6enXSnCL1eEZSVDsy/dYwo/4CczhUsrKLA1SSXB+qce8Glw5DEVtw=="], 317 | 318 | "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 319 | 320 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 321 | 322 | "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], 323 | 324 | "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], 325 | 326 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 327 | 328 | "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], 329 | 330 | "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], 331 | 332 | "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], 333 | 334 | "quansync": ["quansync@0.2.6", "", {}, "sha512-u3TuxVTuJtkTxKGk5oZ7K2/o+l0/cC6J8SOyaaSnrnroqvcVy7xBxtvBUyd+Xa8cGoCr87XmQj4NR6W+zbqH8w=="], 335 | 336 | "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], 337 | 338 | "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], 339 | 340 | "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], 341 | 342 | "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], 343 | 344 | "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], 345 | 346 | "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 347 | 348 | "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 349 | 350 | "rolldown": ["rolldown@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@oxc-project/runtime": "0.71.0", "@oxc-project/types": "0.71.0", "@rolldown/pluginutils": "1.0.0-beta.9-commit.d91dfb5", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-darwin-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-freebsd-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.9-commit.d91dfb5" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-FHkj6gGEiEgmAXQchglofvUUdwj2Oiw603Rs+zgFAnn9Cb7T7z3fiaEc0DbN3ja4wYkW6sF2rzMEtC1V4BGx/g=="], 351 | 352 | "rolldown-plugin-dts": ["rolldown-plugin-dts@0.13.4", "", { "dependencies": { "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1", "ast-kit": "^2.0.0", "birpc": "^2.3.0", "debug": "^4.4.1", "dts-resolver": "^2.0.1", "get-tsconfig": "^4.10.1" }, "peerDependencies": { "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", "vue-tsc": "~2.2.0" }, "optionalPeers": ["typescript", "vue-tsc"] }, "sha512-2+3GnKj6A3wKfyomUKfONRHjgKE85X4PcgW1b84KkHvuN3mUuUiOMseLKafFLMF6NkqQPAJ3FErwtC4HuwIswg=="], 353 | 354 | "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 355 | 356 | "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 357 | 358 | "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], 359 | 360 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 361 | 362 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 363 | 364 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], 365 | 366 | "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], 367 | 368 | "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], 369 | 370 | "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], 371 | 372 | "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 373 | 374 | "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], 375 | 376 | "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], 377 | 378 | "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], 379 | 380 | "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], 381 | 382 | "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], 383 | 384 | "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], 385 | 386 | "tsdown": ["tsdown@0.12.3", "", { "dependencies": { "ansis": "^4.0.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.1", "diff": "^8.0.2", "empathic": "^1.1.0", "hookable": "^5.5.3", "rolldown": "1.0.0-beta.9-commit.d91dfb5", "rolldown-plugin-dts": "^0.13.4", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.13", "unconfig": "^7.3.2" }, "peerDependencies": { "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.js" } }, "sha512-GFqQc6rP8EzsIDs4biOdh7sTveQpMVUv2GUgv7O0Z7yjnlGK050yw0PfUHBHi2xaSyN8K/ztU83DXyEl2upeGw=="], 387 | 388 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 389 | 390 | "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], 391 | 392 | "unconfig": ["unconfig@7.3.2", "", { "dependencies": { "@quansync/fs": "^0.1.1", "defu": "^6.1.4", "jiti": "^2.4.2", "quansync": "^0.2.8" } }, "sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg=="], 393 | 394 | "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 395 | 396 | "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], 397 | 398 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 399 | 400 | "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], 401 | 402 | "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], 403 | 404 | "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], 405 | 406 | "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], 407 | 408 | "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], 409 | 410 | "@quansync/fs/quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="], 411 | 412 | "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], 413 | 414 | "ast-kit/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 415 | 416 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 417 | 418 | "rolldown-plugin-dts/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], 419 | 420 | "tsdown/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], 421 | 422 | "tsdown/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], 423 | 424 | "unconfig/quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="], 425 | 426 | "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], 427 | 428 | "rolldown-plugin-dts/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 429 | 430 | "tsdown/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /example/basic.ts: -------------------------------------------------------------------------------- 1 | import { swagger } from '@elysiajs/swagger' 2 | import { Elysia } from 'elysia' 3 | 4 | import { rateLimit } from '../src' 5 | 6 | const app = new Elysia() 7 | .use(swagger()) 8 | .use(rateLimit()) 9 | .get('/', () => 'hello') 10 | .listen(3000, () => { 11 | console.log('🦊 Swagger is active at: http://localhost:3000/swagger') 12 | }) 13 | -------------------------------------------------------------------------------- /example/multiInstance.ts: -------------------------------------------------------------------------------- 1 | import { swagger } from '@elysiajs/swagger' 2 | import { Elysia } from 'elysia' 3 | import { ip } from 'elysia-ip' // just a glitch pls ignore this 4 | 5 | import { rateLimit } from '../src' 6 | 7 | import type { Generator } from '../src' 8 | 9 | const keyGenerator: Generator<{ ip: string }> = async (req, server, { ip }) => 10 | Bun.hash(JSON.stringify(ip)).toString() 11 | 12 | const aInstance = new Elysia() 13 | .use( 14 | rateLimit({ 15 | scoping: 'scoped', 16 | duration: 200 * 1000, 17 | generator: keyGenerator, 18 | }) 19 | ) 20 | .get('/a', () => 'a') 21 | 22 | const bInstance = new Elysia() 23 | .use( 24 | rateLimit({ 25 | scoping: 'scoped', 26 | duration: 100 * 1000, 27 | generator: keyGenerator, 28 | }) 29 | ) 30 | .get('/b', () => 'b') 31 | 32 | const app = new Elysia() 33 | .use(swagger()) 34 | .use(ip()) 35 | .use(aInstance) 36 | .use(bInstance) 37 | .get('/', () => 'hello') 38 | .listen(3000, () => { 39 | console.log('🦊 Swagger is active at: http://localhost:3000/swagger') 40 | }) 41 | -------------------------------------------------------------------------------- /example/multiInstanceInjected.ts: -------------------------------------------------------------------------------- 1 | import { swagger } from '@elysiajs/swagger' 2 | import { Elysia } from 'elysia' 3 | 4 | import { rateLimit } from '../src' 5 | 6 | import type { Server } from 'bun' 7 | import type { Options } from '../src' 8 | 9 | let server: Server | null 10 | 11 | const options: Partial = { 12 | scoping: 'scoped', 13 | duration: 200 * 1000, 14 | injectServer: () => { 15 | return server! 16 | }, 17 | } 18 | 19 | // const keyGenerator: Generator<{ ip: string }> = async (req, server, { ip }) => Bun.hash(JSON.stringify(ip)).toString() 20 | 21 | const aInstance = new Elysia().use(rateLimit(options)).get('/a', () => 'a') 22 | 23 | const bInstance = new Elysia().use(rateLimit(options)).get('/b', () => 'b') 24 | 25 | const app = new Elysia() 26 | .use(swagger()) 27 | .use(aInstance) 28 | .use(bInstance) 29 | .get('/', () => 'hello') 30 | .listen(3000, () => { 31 | console.log('🦊 Swagger is active at: http://localhost:3000/swagger') 32 | }) 33 | 34 | server = app.server 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elysia-rate-limit", 3 | "version": "4.4.0", 4 | "description": "Rate-limiter for Elysia.js", 5 | "license": "MIT", 6 | "author": { 7 | "name": "rayriffy", 8 | "url": "https://github.com/rayriffy", 9 | "email": "mail@rayriffy.com" 10 | }, 11 | "type": "module", 12 | "module": "./dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "exports": { 15 | "./package.json": "./package.json", 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "import": "./dist/index.js" 19 | } 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/rayriffy/elysia-rate-limit.git" 24 | }, 25 | "publishConfig": { 26 | "access": "public", 27 | "registry": "https://registry.npmjs.org" 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "scripts": { 33 | "dev": "DEBUG=* bun run --hot example/basic.ts", 34 | "dev:multi": "DEBUG=* bun run --hot example/multiInstance.ts", 35 | "dev:inject": "DEBUG=* bun run --hot example/multiInstanceInjected.ts", 36 | "build": "tsdown" 37 | }, 38 | "keywords": [ 39 | "elysia", 40 | "rate", 41 | "limit", 42 | "ratelimit", 43 | "rate-limit" 44 | ], 45 | "dependencies": { 46 | "@alloc/quick-lru": "5.2.0", 47 | "debug": "4.3.4" 48 | }, 49 | "devDependencies": { 50 | "@biomejs/biome": "1.9.4", 51 | "@changesets/cli": "2.28.1", 52 | "@elysiajs/swagger": "1.2.2", 53 | "@tsconfig/node22": "22.0.2", 54 | "@types/bun": "1.2.4", 55 | "@types/debug": "4.1.12", 56 | "elysia": "1.2.23", 57 | "elysia-ip": "1.0.8", 58 | "tsdown": "0.12.3", 59 | "typescript": "5.7.3" 60 | }, 61 | "peerDependencies": { 62 | "elysia": ">= 1.0.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/@types/Context.ts: -------------------------------------------------------------------------------- 1 | import type { MaybePromise } from 'elysia' 2 | import type { Options } from './Options' 3 | 4 | export interface Context { 5 | // class initialization for creating context 6 | init(options: Omit): void 7 | 8 | // function will be called to count request 9 | increment(key: string): MaybePromise<{ 10 | count: number 11 | nextReset: Date 12 | }> 13 | 14 | // function will be called to deduct count in case of request failure 15 | decrement(key: string): MaybePromise 16 | 17 | // if key specified, it will reset count for only specific user, otherwise clear entire storage 18 | reset(key?: string): MaybePromise 19 | 20 | // cleanup function on process killed 21 | kill(): MaybePromise 22 | } 23 | -------------------------------------------------------------------------------- /src/@types/Generator.ts: -------------------------------------------------------------------------------- 1 | import type { MaybePromise } from 'elysia' 2 | import type { Server } from './Server.ts' 3 | 4 | export type Generator = ( 5 | request: Request, 6 | server: Server | null, 7 | derived: T 8 | ) => MaybePromise 9 | -------------------------------------------------------------------------------- /src/@types/Options.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from './Context' 2 | import type { Generator } from './Generator' 3 | import type { Server } from './Server.ts' 4 | 5 | export interface Options { 6 | // The duration for plugin to remember the requests (Default: 60000ms) 7 | duration: number 8 | 9 | // Maximum of requests per specified duration (Default: 10) 10 | max: number 11 | 12 | // Object to response when rate-limit reached 13 | errorResponse: string | Response | Error 14 | 15 | // scoping for rate limiting, set global by default to affect every request, 16 | // but you can adjust to local to affect only within current instance 17 | scoping: 'global' | 'scoped' 18 | 19 | // should the rate limit be counted when a request result is failed (Default: false) 20 | countFailedRequest: boolean 21 | 22 | // key generator function to categorize client for rate-limiting 23 | generator: Generator 24 | 25 | // context for storing requests count 26 | context: Context 27 | 28 | // exposed functions for writing a custom script to skip counting i.e., 29 | // not counting rate limit for some requests 30 | // (Default: always return false) 31 | skip: (req: Request, key?: string) => boolean | Promise 32 | 33 | // an explicit way to inject server instance to generator function 34 | // uses this as last resort only 35 | // since this function will slightly reduce server performance 36 | // (Default: not defined) 37 | injectServer?: () => Server | null 38 | 39 | // let the plugin in control of RateLimit-* headers 40 | // (Default: true) 41 | headers: boolean 42 | } 43 | -------------------------------------------------------------------------------- /src/@types/Server.ts: -------------------------------------------------------------------------------- 1 | import type { Elysia } from 'elysia' 2 | 3 | export type Server = Elysia['server'] 4 | -------------------------------------------------------------------------------- /src/constants/defaultOptions.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { defaultKeyGenerator } from '../services/defaultKeyGenerator' 3 | import { defaultOptions } from './defaultOptions' 4 | 5 | describe('defaultOptions', () => { 6 | it('should have the expected default values', () => { 7 | expect(defaultOptions).toEqual({ 8 | duration: 60000, 9 | max: 10, 10 | errorResponse: 'rate-limit reached', 11 | scoping: 'global', 12 | countFailedRequest: false, 13 | generator: defaultKeyGenerator, 14 | headers: true, 15 | skip: expect.any(Function), 16 | }) 17 | }) 18 | 19 | it('should have a skip function that returns false by default', () => { 20 | const mockRequest = {} as Request 21 | expect(defaultOptions.skip(mockRequest)).toBe(false) 22 | }) 23 | 24 | it('should use defaultKeyGenerator as the generator function', () => { 25 | expect(defaultOptions.generator).toBe(defaultKeyGenerator) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/constants/defaultOptions.ts: -------------------------------------------------------------------------------- 1 | import { defaultKeyGenerator } from '../services/defaultKeyGenerator' 2 | 3 | import type { Options } from '../@types/Options' 4 | 5 | export const defaultOptions: Omit = { 6 | duration: 60000, 7 | max: 10, 8 | errorResponse: 'rate-limit reached', 9 | scoping: 'global', 10 | countFailedRequest: false, 11 | generator: defaultKeyGenerator, 12 | headers: true, 13 | skip: () => false, 14 | } 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { plugin as rateLimit } from './services/plugin' 2 | export { DefaultContext } from './services/defaultContext' 3 | export { defaultOptions } from './constants/defaultOptions' 4 | 5 | export type { Context } from './@types/Context' 6 | export type { Options } from './@types/Options' 7 | export type { Generator } from './@types/Generator' 8 | -------------------------------------------------------------------------------- /src/services/defaultContext.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it } from 'bun:test' 2 | import type { Options } from '../@types/Options' 3 | import { defaultOptions } from '../constants/defaultOptions' 4 | import { DefaultContext } from './defaultContext' 5 | 6 | describe('DefaultContext', () => { 7 | let context: DefaultContext 8 | 9 | beforeEach(() => { 10 | context = new DefaultContext() 11 | context.init({ 12 | ...defaultOptions, 13 | }) 14 | }) 15 | 16 | afterEach(async () => { 17 | await context.kill() 18 | }) 19 | 20 | it('should initialize with default maxSize', () => { 21 | const ctx = new DefaultContext() 22 | expect(ctx).toBeInstanceOf(DefaultContext) 23 | }) 24 | 25 | it('should initialize with custom maxSize', () => { 26 | const ctx = new DefaultContext(1000) 27 | expect(ctx).toBeInstanceOf(DefaultContext) 28 | }) 29 | 30 | it('should increment counter for new key', async () => { 31 | const key = 'test-key-1' 32 | const result = await context.increment(key) 33 | 34 | expect(result.count).toBe(1) 35 | expect(result.nextReset).toBeInstanceOf(Date) 36 | expect(result.nextReset.getTime()).toBeGreaterThan(Date.now()) 37 | }) 38 | 39 | it('should increment counter for existing key', async () => { 40 | const key = 'test-key-2' 41 | 42 | await context.increment(key) 43 | const result = await context.increment(key) 44 | 45 | expect(result.count).toBe(2) 46 | }) 47 | 48 | it('should decrement counter', async () => { 49 | const key = 'test-key-3' 50 | 51 | await context.increment(key) 52 | await context.increment(key) 53 | await context.decrement(key) 54 | 55 | const result = await context.increment(key) 56 | expect(result.count).toBe(2) // It should be 2 after decrement and new increment 57 | }) 58 | 59 | it('should reset counter for specific key', async () => { 60 | const key1 = 'test-key-4' 61 | const key2 = 'test-key-5' 62 | 63 | await context.increment(key1) 64 | await context.increment(key2) 65 | await context.reset(key1) 66 | 67 | const result1 = await context.increment(key1) 68 | expect(result1.count).toBe(1) // Should be reset 69 | 70 | const result2 = await context.increment(key2) 71 | expect(result2.count).toBe(2) // Should still be incremented 72 | }) 73 | 74 | it('should reset all counters', async () => { 75 | const key1 = 'test-key-6' 76 | const key2 = 'test-key-7' 77 | 78 | await context.increment(key1) 79 | await context.increment(key2) 80 | await context.reset() 81 | 82 | const result1 = await context.increment(key1) 83 | expect(result1.count).toBe(1) 84 | 85 | const result2 = await context.increment(key2) 86 | expect(result2.count).toBe(1) 87 | }) 88 | 89 | it('should handle expired keys correctly', async () => { 90 | // Create context with a very short duration 91 | const shortContext = new DefaultContext() 92 | shortContext.init({ 93 | ...defaultOptions, 94 | duration: 10, 95 | } as Omit) 96 | 97 | const key = 'test-key-8' 98 | 99 | await shortContext.increment(key) 100 | 101 | // Wait for the timeout to expire 102 | await new Promise(resolve => setTimeout(resolve, 15)) 103 | 104 | // After expiration, the count should reset 105 | const result = await shortContext.increment(key) 106 | expect(result.count).toBe(1) 107 | 108 | await shortContext.kill() 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /src/services/defaultContext.ts: -------------------------------------------------------------------------------- 1 | import AllocQuickLRU from '@alloc/quick-lru' 2 | 3 | import type { Context } from '../@types/Context' 4 | import type { Options } from '../@types/Options' 5 | import { logger } from './logger' 6 | 7 | interface Item { 8 | count: number 9 | nextReset: Date 10 | } 11 | 12 | export class DefaultContext implements Context { 13 | private readonly id: string = (Math.random() + 1).toString(36).substring(7) 14 | private readonly maxSize: number 15 | private store!: AllocQuickLRU 16 | private duration!: number 17 | 18 | public constructor(maxSize = 5000) { 19 | this.maxSize = maxSize 20 | } 21 | 22 | public init(options: Omit) { 23 | logger( 24 | `context:${this.id}`, 25 | 'initialized with maxSize: %d, and expire duration of %d seconds', 26 | this.maxSize, 27 | options.duration / 1000 28 | ) 29 | 30 | this.duration = options.duration 31 | this.store = new AllocQuickLRU({ 32 | maxSize: this.maxSize, 33 | }) 34 | } 35 | 36 | public async increment(key: string) { 37 | const now = new Date() 38 | let item = this.store.get(key) 39 | 40 | // if item is not found or expired, then issue a new one 41 | if (item === undefined || item.nextReset < now) { 42 | logger( 43 | `context:${this.id}`, 44 | 'created new item for key: %s (reason: %s)', 45 | key, 46 | item === undefined ? 'not found' : 'expired' 47 | ) 48 | 49 | item = { 50 | count: 1, 51 | nextReset: new Date(now.getTime() + this.duration), 52 | } 53 | } 54 | // otherwise, increment the count 55 | else { 56 | logger(`context:${this.id}`, 'incremented count for key: %s', key) 57 | 58 | item.count++ 59 | } 60 | 61 | // update the store 62 | this.store.set(key, item) 63 | 64 | return item 65 | } 66 | 67 | public async decrement(key: string) { 68 | const item = this.store.get(key) 69 | 70 | // perform actions only if an item is found 71 | if (item !== undefined) { 72 | logger(`context:${this.id}`, 'decremented count for key: %s', key) 73 | 74 | // decrement the count by 1 75 | item.count-- 76 | 77 | // update the store 78 | this.store.set(key, item) 79 | } 80 | } 81 | 82 | public async reset(key?: string) { 83 | logger(`context:${this.id}`, 'resetting target %s', key ?? 'all') 84 | 85 | if (typeof key === 'string') this.store.delete(key) 86 | else this.store.clear() 87 | } 88 | 89 | public kill() { 90 | logger(`context:${this.id}`, 'clearing the store') 91 | 92 | this.store.clear() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/services/defaultKeyGenerator.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock } from 'bun:test' 2 | import type { Server } from '../@types/Server.ts' 3 | import { defaultKeyGenerator } from './defaultKeyGenerator' 4 | 5 | describe('defaultKeyGenerator', () => { 6 | it('should return client IP address', () => { 7 | const mockRequest = {} as Request 8 | const mockServer = { 9 | requestIP: mock(() => ({ address: '192.168.1.1' })), 10 | } 11 | 12 | const key = defaultKeyGenerator( 13 | mockRequest, 14 | mockServer as unknown as Server, 15 | {} 16 | ) 17 | 18 | expect(key).toBe('192.168.1.1') 19 | expect(mockServer.requestIP).toHaveBeenCalledWith(mockRequest) 20 | }) 21 | 22 | it('should return empty string when IP address is undefined', () => { 23 | const mockRequest = {} as Request 24 | const mockServer = { 25 | requestIP: mock(() => ({ address: undefined })), 26 | } 27 | 28 | // Mock console.warn to avoid output during tests 29 | const originalWarn = console.warn 30 | console.warn = mock(() => {}) 31 | 32 | const key = defaultKeyGenerator( 33 | mockRequest, 34 | mockServer as unknown as Server, 35 | {} 36 | ) 37 | 38 | expect(key).toBe('') 39 | expect(mockServer.requestIP).toHaveBeenCalledWith(mockRequest) 40 | expect(console.warn).toHaveBeenCalled() 41 | 42 | // Restore console.warn 43 | console.warn = originalWarn 44 | }) 45 | 46 | it('should return empty string when requestIP returns null', () => { 47 | const mockRequest = {} as Request 48 | const mockServer = { 49 | requestIP: mock(() => null), 50 | } 51 | 52 | // Mock console.warn to avoid output during tests 53 | const originalWarn = console.warn 54 | console.warn = mock(() => {}) 55 | 56 | const key = defaultKeyGenerator( 57 | mockRequest, 58 | mockServer as unknown as Server, 59 | {} 60 | ) 61 | 62 | expect(key).toBe('') 63 | expect(mockServer.requestIP).toHaveBeenCalledWith(mockRequest) 64 | expect(console.warn).toHaveBeenCalled() 65 | 66 | // Restore console.warn 67 | console.warn = originalWarn 68 | }) 69 | 70 | it('should return empty string when server is null', () => { 71 | const mockRequest = {} as Request 72 | 73 | // Mock console.warn to avoid output during tests 74 | const originalWarn = console.warn 75 | console.warn = mock(() => {}) 76 | 77 | const key = defaultKeyGenerator(mockRequest, null, {}) 78 | 79 | expect(key).toBe('') 80 | expect(console.warn).toHaveBeenCalled() 81 | 82 | // Restore console.warn 83 | console.warn = originalWarn 84 | }) 85 | 86 | it('should return empty string when request is undefined', () => { 87 | // Mock console.warn to avoid output during tests 88 | const originalWarn = console.warn 89 | console.warn = mock(() => {}) 90 | 91 | const key = defaultKeyGenerator(undefined as unknown as Request, null, {}) 92 | 93 | expect(key).toBe('') 94 | expect(console.warn).toHaveBeenCalled() 95 | 96 | // Restore console.warn 97 | console.warn = originalWarn 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/services/defaultKeyGenerator.ts: -------------------------------------------------------------------------------- 1 | import { logger } from './logger' 2 | 3 | import type { Generator } from '../@types/Generator' 4 | 5 | export const defaultKeyGenerator: Generator = (request, server): string => { 6 | if (!server || !request) { 7 | console.warn( 8 | '[elysia-rate-limit] failed to determine client address (reason: server or request is undefined)' 9 | ) 10 | return '' 11 | } 12 | 13 | // Get the IP info once to avoid redundant calls 14 | const requestIpResult = server.requestIP(request) 15 | const clientAddress = requestIpResult?.address 16 | 17 | logger('generator', 'clientAddress: %s', clientAddress) 18 | 19 | if (clientAddress === undefined) { 20 | let reason: string 21 | 22 | if (requestIpResult === null) reason = '.requestIP() returns null' 23 | else if (requestIpResult.address === undefined) 24 | reason = '.requestIP()?.address returns undefined' 25 | else reason = 'unknown' 26 | 27 | console.warn( 28 | `[elysia-rate-limit] failed to determine client address (reason: ${reason})` 29 | ) 30 | 31 | return '' 32 | } 33 | 34 | return clientAddress 35 | } 36 | -------------------------------------------------------------------------------- /src/services/logger.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | 3 | // create a cache of debug instances to avoid creating them on every call 4 | const debugCache = new Map() 5 | 6 | export const logger = (unit: string, formatter: any, ...params: any[]) => { 7 | const key = `elysia-rate-limit:${unit}` 8 | 9 | let debugInstance = debugCache.get(key) 10 | if (!debugInstance) { 11 | debugInstance = debug(key) 12 | debugCache.set(key, debugInstance) 13 | } 14 | 15 | debugInstance(formatter, ...params) 16 | } 17 | -------------------------------------------------------------------------------- /src/services/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock } from 'bun:test' 2 | import { Elysia } from 'elysia' 3 | import type { Options } from '../@types/Options' 4 | import { DefaultContext } from './defaultContext' 5 | import { plugin } from './plugin' 6 | 7 | describe('rate limit plugin', () => { 8 | it('should initialize with default options', () => { 9 | const app = new Elysia() 10 | const rateLimitPlugin = plugin() 11 | const appWithPlugin = rateLimitPlugin(app) 12 | 13 | expect(appWithPlugin).toBeInstanceOf(Elysia) 14 | }) 15 | 16 | it('should accept custom options', () => { 17 | const app = new Elysia() 18 | const customContext = new DefaultContext() 19 | const initSpy = mock((options: Omit) => {}) 20 | customContext.init = initSpy 21 | 22 | const rateLimitPlugin = plugin({ 23 | max: 10, 24 | duration: 60000, 25 | context: customContext, 26 | }) 27 | 28 | const appWithPlugin = rateLimitPlugin(app) 29 | 30 | expect(appWithPlugin).toBeInstanceOf(Elysia) 31 | expect(initSpy).toHaveBeenCalled() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/services/plugin.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | 3 | import { defaultOptions } from '../constants/defaultOptions' 4 | import { DefaultContext } from './defaultContext' 5 | 6 | import { logger } from './logger' 7 | 8 | import type { Options } from '../@types/Options' 9 | 10 | export const plugin = function rateLimitPlugin(userOptions?: Partial) { 11 | const options: Options = { 12 | ...defaultOptions, 13 | ...userOptions, 14 | context: userOptions?.context ?? new DefaultContext(), 15 | } 16 | 17 | options.context.init(options) 18 | 19 | // NOTE: 20 | // do not make plugin to return async 21 | // otherwise request will be triggered twice 22 | return function registerRateLimitPlugin(app: Elysia) { 23 | const plugin = new Elysia({ 24 | name: 'elysia-rate-limit', 25 | seed: options.max, 26 | }) 27 | 28 | plugin.onBeforeHandle( 29 | { as: options.scoping }, 30 | async function onBeforeHandleRateLimitHandler({ 31 | set, 32 | request, 33 | query, 34 | path, 35 | store, 36 | cookie, 37 | error, 38 | body, 39 | params, 40 | headers, 41 | // @ts-expect-error somehow qi is being sent from elysia, but there's no type declaration for it 42 | qi, 43 | ...rest 44 | }) { 45 | let clientKey: string | undefined 46 | 47 | /** 48 | * if a skip option has two parameters, 49 | * then we will generate clientKey ahead of time. 50 | * this is made to skip generating key unnecessary if only check for request 51 | * and saving some cpu consumption when actually skipped 52 | */ 53 | if (options.skip.length >= 2) 54 | clientKey = await options.generator( 55 | request, 56 | options.injectServer?.() ?? app.server, 57 | rest 58 | ) 59 | 60 | // if decided to skip, then do nothing and let the app continue 61 | if ((await options.skip(request, clientKey)) === false) { 62 | /** 63 | * if a skip option has less than two parameters, 64 | * that's mean clientKey does not have a key yet 65 | * then generate one 66 | */ 67 | if (options.skip.length < 2) 68 | clientKey = await options.generator( 69 | request, 70 | options.injectServer?.() ?? app.server, 71 | rest 72 | ) 73 | 74 | const { count, nextReset } = await options.context.increment( 75 | // biome-ignore lint/style/noNonNullAssertion: 76 | clientKey! 77 | ) 78 | 79 | const payload = { 80 | limit: options.max, 81 | current: count, 82 | remaining: Math.max(options.max - count, 0), 83 | nextReset, 84 | } 85 | 86 | // set standard headers 87 | const reset = Math.max( 88 | 0, 89 | Math.ceil((nextReset.getTime() - Date.now()) / 1000) 90 | ) 91 | 92 | const builtHeaders: Record = { 93 | 'RateLimit-Limit': String(options.max), 94 | 'RateLimit-Remaining': String(payload.remaining), 95 | 'RateLimit-Reset': String(reset), 96 | } 97 | 98 | // reject if limit were reached 99 | if (payload.current >= payload.limit + 1) { 100 | logger( 101 | 'plugin', 102 | 'rate limit exceeded for clientKey: %s (resetting in %d seconds)', 103 | clientKey, 104 | reset 105 | ) 106 | 107 | builtHeaders['Retry-After'] = String( 108 | Math.ceil(options.duration / 1000) 109 | ) 110 | 111 | if (options.errorResponse instanceof Error) 112 | throw options.errorResponse 113 | if (options.errorResponse instanceof Response) { 114 | // duplicate the response to avoid mutation 115 | const clonedResponse = options.errorResponse.clone() 116 | 117 | // append headers 118 | if (options.headers) 119 | for (const [key, value] of Object.entries(builtHeaders)) 120 | clonedResponse.headers.set(key, value) 121 | 122 | return clonedResponse 123 | } 124 | 125 | // append headers 126 | if (options.headers) 127 | for (const [key, value] of Object.entries(builtHeaders)) 128 | set.headers[key] = value 129 | 130 | // set default status code 131 | set.status = 429 132 | 133 | return options.errorResponse 134 | } 135 | 136 | // append headers 137 | if (options.headers) 138 | for (const [key, value] of Object.entries(builtHeaders)) 139 | set.headers[key] = value 140 | 141 | logger( 142 | 'plugin', 143 | 'clientKey %s passed through with %d/%d request used (resetting in %d seconds)', 144 | clientKey, 145 | options.max - payload.remaining, 146 | options.max, 147 | reset 148 | ) 149 | } 150 | } 151 | ) 152 | 153 | plugin.onError( 154 | { as: options.scoping }, 155 | async function onErrorRateLimitHandler({ 156 | set, 157 | request, 158 | query, 159 | path, 160 | store, 161 | cookie, 162 | error, 163 | body, 164 | params, 165 | headers, 166 | // @ts-expect-error somehow qi is being sent from elysia, but there's no type declaration for it 167 | qi, 168 | code, 169 | ...rest 170 | }) { 171 | if (!options.countFailedRequest) { 172 | const clientKey = await options.generator( 173 | request, 174 | options.injectServer?.() ?? app.server, 175 | rest 176 | ) 177 | 178 | logger( 179 | 'plugin', 180 | 'request failed for clientKey: %s, refunding', 181 | clientKey 182 | ) 183 | await options.context.decrement(clientKey) 184 | } 185 | } 186 | ) 187 | 188 | plugin.onStop(async function onStopRateLimitHandler() { 189 | logger('plugin', 'kill signal received') 190 | await options.context.kill() 191 | }) 192 | 193 | return app.use(plugin) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "moduleResolution": "Bundler", 6 | "module": "ESNext", 7 | "lib": ["DOM", "ES2015"], 8 | "verbatimModuleSyntax": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm'], 6 | dts: true, 7 | sourcemap: true, 8 | clean: true, 9 | external: ['elysia', 'debug', '@alloc/quick-lru'], 10 | }) 11 | --------------------------------------------------------------------------------