├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 | [](https://www.npmjs.com/package/elysia-rate-limit)
6 | [](https://www.npmjs.com/package/elysia-rate-limit)
7 | [](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 |
--------------------------------------------------------------------------------