Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: express-rate-limit/express-rate-limit
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 67d365f552a1412a1657bbb318027615885f9a01
Choose a base ref
...
head repository: express-rate-limit/express-rate-limit
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: a8dc1f7b38b47b219a6578b455129fc180634ba9
Choose a head ref

Commits on Jul 22, 2021

  1. Copy the full SHA
    0dfd0be View commit details

Commits on Oct 12, 2021

  1. Merge pull request #245 from takeshirs/feature/handler-options

    allow handler to access options
    
    fixes #214
    nfriedly authored Oct 12, 2021
    Copy the full SHA
    5cf1129 View commit details
  2. Copy the full SHA
    184eee8 View commit details
  3. 5.5.0

    nfriedly committed Oct 12, 2021
    Copy the full SHA
    bbeca82 View commit details

Commits on Nov 6, 2021

  1. link to precise-memory-rate-limit

    relates to #257
    nfriedly authored Nov 6, 2021
    Copy the full SHA
    e69d0a7 View commit details
  2. call out AWS API Gateway

    relates to #260
    nfriedly authored Nov 6, 2021
    Copy the full SHA
    03bb47c View commit details
  3. Copy the full SHA
    5fb38fa View commit details
  4. 5.5.1

    nfriedly committed Nov 6, 2021
    Copy the full SHA
    b9e0e59 View commit details

Commits on Nov 30, 2021

  1. Copy the full SHA
    98f4a6b View commit details
  2. declare RateLimit fct type

    kevin-krug authored and gamemaker1 committed Nov 30, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    a2c6f3a View commit details
  3. declare export

    kevin-krug authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    83c616e View commit details
  4. Convert to typescript

    googol authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    36dac64 View commit details
  5. Copy the full SHA
    d0155bf View commit details
  6. inline sources into sourcemaps

    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    66d26e3 View commit details
  7. dep bump

    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    fbc8a5d View commit details
  8. Copy the full SHA
    1ae18c6 View commit details
  9. adding a typing test to attepot to repro reported issue. Test should …

    …be failing but isn't :(
    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    116d188 View commit details
  10. adding a typing test to attepot to repro reported issue. Test should …

    …be failing but isn't :(
    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    9a03733 View commit details
  11. dropping node.js version 6 because eslint dropped v6 support which pr…

    …events the tests from passing
    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    63cfc9c View commit details
  12. fix message type, add a typings test

    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    bffd50d View commit details
  13. fix message type and typings test

    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    42cc550 View commit details
  14. 0.0.0-typescript-beta-5

    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    41d4d3e View commit details
  15. Copy the full SHA
    31c6540 View commit details
  16. fixing the message type again. not sure how it got unfixed, but at le…

    …ast the tests are catching it now
    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    2c6128e View commit details
  17. 0.0.0-typescript-beta-6

    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    66d8918 View commit details
  18. Copy the full SHA
    14a9fb1 View commit details
  19. Mark interface properties readonly

    googol authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    366cf83 View commit details
  20. Copy the full SHA
    45d7dec View commit details
  21. Add helper function getRateLimit

    In typescript any additions to the Request or Response interfaces from
    express are very difficult. With a helper function like this, accessing
    the current rate limit info in a type safe way is much easier.
    googol authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    a3a53de View commit details
  22. Add typings for supertest

    googol authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    3991809 View commit details
  23. Add tsconfig for tests

    This way the tests written in ts will also have the same compiler
    options for strictness etc as the main code.
    googol authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    f02cc15 View commit details
  24. Add typescript notice to README

    googol authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    a3e8b39 View commit details
  25. Copy the full SHA
    3ec1a15 View commit details
  26. naming

    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    b605230 View commit details
  27. 0.0.0-typescript-beta-7

    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    64f24af View commit details
  28. update deploy version of node

    nfriedly authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    6d44a34 View commit details
  29. Copy the full SHA
    e150068 View commit details
  30. synced tests with master

    shilangyu authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    8ea0066 View commit details
  31. added license

    shilangyu authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    6f89fbb View commit details
  32. Copy the full SHA
    0995e56 View commit details
  33. draft polli

    shilangyu authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    7c1ebcb View commit details
  34. typescript imports

    shilangyu authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    b779e65 View commit details
  35. rebase tests, readme, and license

    shilangyu authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    56c44e3 View commit details
  36. Allow skip to be a promise

    shilangyu authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    d8cf485 View commit details
  37. npm run autofix

    shilangyu authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    be2b10d View commit details
  38. deps bump

    shilangyu authored and gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    4bb1bbb View commit details
  39. meta: prettify code

    - add tsdoc comments to code
    - run prettier
    - bump dependencies
    gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    931390e View commit details
  40. meta: add missing features and convert tests to ts

    - convert to esm module, don't test on node 12
    - use jest instead of mocha for tests
    - add features from js branch
    gamemaker1 committed Nov 30, 2021
    Copy the full SHA
    610e951 View commit details
  41. Copy the full SHA
    43b7023 View commit details
  42. Copy the full SHA
    61f4372 View commit details
Showing with 23,584 additions and 4,258 deletions.
  1. +16 −6 .editorconfig
  2. +0 −3 .eslintignore
  3. +0 −26 .eslintrc.js
  4. +4 −0 .gitattributes
  5. +49 −0 .github/ISSUE_TEMPLATE/bug_report.yml
  6. +1 −0 .github/ISSUE_TEMPLATE/config.yml
  7. +30 −0 .github/ISSUE_TEMPLATE/feature_request.yml
  8. +57 −0 .github/pull_request_template.md
  9. +105 −0 .github/workflows/ci.yaml
  10. +0 −45 .github/workflows/publish.yml
  11. +0 −27 .github/workflows/test.yml
  12. +14 −3 .gitignore
  13. +0 −7 LICENSE
  14. +0 −349 README.md
  15. +177 −0 changelog.md
  16. +4 −0 config/husky/pre-commit
  17. +222 −0 contributing.md
  18. +0 −190 lib/express-rate-limit.js
  19. +0 −47 lib/memory-store.js
  20. +20 −0 license.md
  21. +19,133 −2,501 package-lock.json
  22. +142 −51 package.json
  23. +473 −0 readme.md
  24. +13 −0 source/index.ts
  25. +331 −0 source/lib.ts
  26. +121 −0 source/memory-store.ts
  27. +318 −0 source/types.ts
  28. +0 −734 test/express-rate-limit-test.js
  29. +7 −0 test/external/imports/default-import/js-cjs/.gitignore
  30. +41 −0 test/external/imports/default-import/js-cjs/package.json
  31. +21 −0 test/external/imports/default-import/js-cjs/source/app.js
  32. +8 −0 test/external/imports/default-import/js-cjs/source/index.js
  33. +12 −0 test/external/imports/default-import/js-cjs/test/server-test.js
  34. +7 −0 test/external/imports/default-import/js-esm/.gitignore
  35. +44 −0 test/external/imports/default-import/js-esm/package.json
  36. +20 −0 test/external/imports/default-import/js-esm/source/app.js
  37. +8 −0 test/external/imports/default-import/js-esm/source/index.js
  38. +12 −0 test/external/imports/default-import/js-esm/test/server-test.js
  39. +7 −0 test/external/imports/default-import/ts-cjs/.gitignore
  40. +63 −0 test/external/imports/default-import/ts-cjs/package.json
  41. +46 −0 test/external/imports/default-import/ts-cjs/source/app.ts
  42. +8 −0 test/external/imports/default-import/ts-cjs/source/index.ts
  43. +12 −0 test/external/imports/default-import/ts-cjs/test/server-test.ts
  44. +9 −0 test/external/imports/default-import/ts-cjs/tsconfig.json
  45. +7 −0 test/external/imports/default-import/ts-esm/.gitignore
  46. +66 −0 test/external/imports/default-import/ts-esm/package.json
  47. +46 −0 test/external/imports/default-import/ts-esm/source/app.ts
  48. +8 −0 test/external/imports/default-import/ts-esm/source/index.ts
  49. +12 −0 test/external/imports/default-import/ts-esm/test/server-test.ts
  50. +9 −0 test/external/imports/default-import/ts-esm/tsconfig.json
  51. +7 −0 test/external/imports/named-import/js-cjs/.gitignore
  52. +41 −0 test/external/imports/named-import/js-cjs/package.json
  53. +20 −0 test/external/imports/named-import/js-cjs/source/app.js
  54. +8 −0 test/external/imports/named-import/js-cjs/source/index.js
  55. +12 −0 test/external/imports/named-import/js-cjs/test/server-test.js
  56. +7 −0 test/external/imports/named-import/js-esm/.gitignore
  57. +44 −0 test/external/imports/named-import/js-esm/package.json
  58. +20 −0 test/external/imports/named-import/js-esm/source/app.js
  59. +8 −0 test/external/imports/named-import/js-esm/source/index.js
  60. +12 −0 test/external/imports/named-import/js-esm/test/server-test.js
  61. +7 −0 test/external/imports/named-import/ts-cjs/.gitignore
  62. +63 −0 test/external/imports/named-import/ts-cjs/package.json
  63. +47 −0 test/external/imports/named-import/ts-cjs/source/app.ts
  64. +8 −0 test/external/imports/named-import/ts-cjs/source/index.ts
  65. +12 −0 test/external/imports/named-import/ts-cjs/test/server-test.ts
  66. +9 −0 test/external/imports/named-import/ts-cjs/tsconfig.json
  67. +7 −0 test/external/imports/named-import/ts-esm/.gitignore
  68. +66 −0 test/external/imports/named-import/ts-esm/package.json
  69. +47 −0 test/external/imports/named-import/ts-esm/source/app.ts
  70. +8 −0 test/external/imports/named-import/ts-esm/source/index.ts
  71. +12 −0 test/external/imports/named-import/ts-esm/test/server-test.ts
  72. +9 −0 test/external/imports/named-import/ts-esm/tsconfig.json
  73. +50 −0 test/external/run-all-tests
  74. +7 −0 test/external/stores/.gitignore
  75. +50 −0 test/external/stores/package.json
  76. +22 −0 test/external/stores/source/memcached-store.ts
  77. +26 −0 test/external/stores/source/mongo-store.ts
  78. +23 −0 test/external/stores/source/precise-store.ts
  79. +22 −0 test/external/stores/source/redis-store.ts
  80. +21 −0 test/external/stores/test/stores-test.ts
  81. +9 −0 test/external/stores/tsconfig.json
  82. +0 −81 test/headers-test.js
  83. +65 −0 test/library/headers-test.ts
  84. +41 −0 test/library/helpers/create-server.ts
  85. +126 −0 test/library/memory-store-test.ts
  86. +857 −0 test/library/middleware-test.ts
  87. +75 −0 test/library/options-test.ts
  88. +0 −188 test/memory-store-test.js
  89. +13 −0 tsconfig.json
22 changes: 16 additions & 6 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
# EditorConfig: automatically set indentation and such
# http://EditorConfig.org
# /.editorconfig
# Tells most editors what our style preferences are
# https://editorconfig.org

# top-most EditorConfig file
root = true

[*.{js,json}]
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 80
indent_size = tab
tab_width = 2

[*.{ts,json,md}]
indent_style = tab

[*.yaml]
indent_style = spaces
3 changes: 0 additions & 3 deletions .eslintignore

This file was deleted.

26 changes: 0 additions & 26 deletions .eslintrc.js

This file was deleted.

4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# /.gitattributes
# Makes sure all line endings are LF

* text=auto eol=lf
49 changes: 49 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Report a bug
description: ———
labels: [bug]
body:
- type: markdown
attributes:
value: |
# Thanks for reporting this bug!
Help us replicate and find a fix for the issue by filling in this form.
- type: textarea
attributes:
label: Description
description: |
Describe the issue and how to replicate it. If possible, please include
a minimal example to reproduce the issue.
validations:
required: true
- type: input
attributes:
label: Library version
description: |
Can be found the `express-rate-limit` field in the `dependencies` map of
`package.json`
validations:
required: true
- type: input
attributes:
label: Node version
description: Output of the `node --version` command
validations:
required: true
- type: input
attributes:
label: Typescript version (if you are using it)
description: |
Can be found the `typescript` field in the
`dependencies`/`devDependencies` map of `package.json`
- type: dropdown
attributes:
label: Module system
description: |
If you have `type: module` in your `package.json` file, then you are
using ESM, else you are using CommonJS.
options:
- ESM
- CommonJS
validations:
required: true
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
blank_issues_enabled: false
30 changes: 30 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Suggest an improvement or new feature
description: ———
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
# Thanks for filing this feature request!
Help us understanding this feature and the need for it better by filling in this form.
- type: textarea
attributes:
label: Description
description: Describe the feature in detail
validations:
required: true
- type: textarea
attributes:
label: Why
description:
Why should we add this feature? What are potential use cases for it?
validations:
required: true
- type: textarea
attributes:
label: Alternatives
description:
Describe the alternatives you have considered, or existing workarounds
validations:
required: true
57 changes: 57 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!--
Hi there! Thanks for contributing! Please fill in this template to help us
review and merge the PR as quickly and easily as possible!
-->

## Related Issues

<!--
If this is a bug fix, or adds a feature mentioned in another issue, mention
it as follows:
- Closes #10
- Fixes #15
-->

## What Does This PR Do?

<!--
Explain what has been added/changed/removed, in
[keepachangelog.com](https://keepachangelog.com) style.
-->

### Added

<!--
- Added a new method on the limiter object to reset the count for a certain IP [#10]
-->

### Changed

<!--
- Deprecated `global` option
- Fixed test for deprecated options [#15]
-->

### Removed

<!--
- Removed deprecated `headers` option
-->

## Caveats/Problems/Issues

<!--
Any weird code/problems you faced while making this PR. Feel free to ask for
help with anything, especially if it's your first time contributing!
-->

## Checklist

- [ ] The issues that this PR fixes/closes have been mentioned above.
- [ ] What this PR adds/changes/removes has been explained.
- [ ] All tests (`npm test`) pass.
- [ ] All added/modified code has been commented, and
methods/classes/constants/types have been annotated with TSDoc comments.
- [ ] If a new feature has been added or a bug has been fixed, tests have been
added for the same.
105 changes: 105 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# /.github/workflows/ci.yaml
# GitHub actions workflow

name: CI

on: [push, pull_request]

jobs:
lint:
name: Lint
strategy:
matrix:
node-version: [16.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Use Node ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Check for lint/formatting errors
run: |
npm ci
npm run lint
test_lib:
name: Test (Library)
strategy:
matrix:
node-version: [14.x, 15.x, 16.x, 17.x]
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Use Node ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Run library tests
run: |
npm ci
npm run test:lib
test_ext:
name: Test (External)
strategy:
matrix:
node-version: [14.x, 15.x, 16.x, 17.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
services:
rate-limit-redis:
image: redis
ports:
- 6379:6379
rate-limit-mongo:
image: mongo
ports:
- 27017:27017
rate-limit-memcached:
image: memcached
ports:
- 11211:11211
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Use Node ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Run import and external store usage tests
run: |
npm ci
npm pack
npm run test:ext
publish:
name: Publish
needs: [lint, test_lib, test_ext]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 16
registry-url: https://registry.npmjs.org/
- name: Install dependencies
run: npm ci
- name: Publish package to NPM
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Build package to upload to GitHub releases
run: |
npm pack
mv express-rate-limit-*.tgz express-rate-limit.tgz
- name: Create a Github release
uses: softprops/action-gh-release@v1
with:
files: express-rate-limit.tgz
body:
You can view the changelog
[here](https://github.com/nfriedly/express-rate-limit/blob/master/changelog.md).
45 changes: 0 additions & 45 deletions .github/workflows/publish.yml

This file was deleted.

27 changes: 0 additions & 27 deletions .github/workflows/test.yml

This file was deleted.

17 changes: 14 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
/node_modules/
npm-debug.log
# /.gitignore
# Tells Git to ignore these files

node_modules/
dist/
coverage/
test/external/**/package-lock.json

.vscode/
.idea/
.vscode/

*.log
*.tmp
*.bak
*.tgz
7 changes: 0 additions & 7 deletions LICENSE

This file was deleted.

349 changes: 0 additions & 349 deletions README.md

This file was deleted.

177 changes: 177 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [6.2.0](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.2.0)

### Added

- Export the `MemoryStore`, so it can now be imported as a named import
(`import { MemoryStore } from 'express-rate-limit'`).

### Fixed

- Deprecate the `onLimitReached` option (this was supposed to be deprecated in
v6.0.0 itself); developers should use a custom handler function that checks if
the rate limit has been exceeded instead.

## [6.1.0](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.1.0)

### Added

- Added a named export `rateLimit` in case the default import does not work.

### Fixed

- Added a named export `default`, so Typescript CommonJS developers can
default-import the library (`import rateLimit from 'express-rate-limit'`).

## [6.0.5](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.0.5)

### Fixed

- Use named imports for ExpressJS types so users do not need to enable the
`esModuleInterop` flag in their Typescript compiler configuration.

## [6.0.4](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.0.4)

### Fixed

- Upload the built package as a `.tgz` to GitHub releases.

### Changed

- Add ` main` and `module` fields to `package.json`. This helps tools such as
ESLint that do not yet support the `exports` field.
- Bumped the minimum node.js version in `package-lock.json` to match
`package.json`

## [6.0.3](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.0.3)

### Changed

- Bumped minimum Node version from 12.9 to 14.5 in `package.json` because the
transpiled output uses the nullish coalescing operator (`??`), which
[isn't supported in node.js prior to 14.x](https://node.green/#ES2020-features--nullish-coalescing-operator-----).

## [6.0.2](https://github.com/nfriedly/express-rate-limit/releases/v6.0.2)

### Fixed

- Ensure CommonJS projects can import the module.

### Added

- Add additional tests that test:
- importing the library in `js-cjs`, `js-esm`, `ts-cjs`, `ts-esm`
environments.
- usage of the library with external stores (`redis`, `mongo`, `memcached`,
`precise`).

### Changed

- Use [`esbuild`](https://esbuild.github.io/) to generate ESM and CJS output.
This reduces the size of the built package from 138 kb to 13kb and build time
to 4 ms! :rocket:
- Use [`dts-bundle-generator`](https://github.com/timocov/dts-bundle-generator)
to generate a single Typescript declaration file.

## [6.0.1](https://github.com/nfriedly/express-rate-limit/releases/v6.0.1)

### Fixed

- Ensure CommonJS projects can import the module.

## [6.0.0](https://github.com/nfriedly/express-rate-limit/releases/v6.0.0)

### Added

- `express` 4.x as a peer dependency.
- Better Typescript support (the library was rewritten in Typescript).
- Export the package as both ESM and CJS.
- Publish the built package (`.tgz` file) on GitHub releases as well as the npm
registry.
- Issue and PR templates.
- A contributing guide.

### Changed

- Rename the `draft_polli_ratelimit_headers` option to `standardHeaders`.
- Rename the `headers` option to `legacyHeaders`.
- `Retry-After` header is now sent if either `legacyHeaders` or
`standardHeaders` is set.
- Allow `keyGenerator` to be an async function/return a promise.
- Change the way custom stores are defined.
- Add the `init` method for stores to set themselves up using options passed
to the middleware.
- Rename the `incr` method to `increment`.
- Allow the `increment`, `decrement`, `resetKey` and `resetAll` methods to
return a promise.
- Old stores will automatically be promisified and used.
- The package can now only be used with NodeJS version 12.9.0 or greater.
- The `onLimitReached` configuration option is now deprecated. Replace it with a
custom `handler` that checks the number of hits.

### Removed

- Remove the deprecated `limiter.resetIp` method (use the `limiter.resetKey`
method instead).
- Remove the deprecated options `delayMs`, `delayAfter` (the delay functionality
was moved to the
[`express-slow-down`](https://github.com/nfriedly/express-slow-down) package)
and `global` (use a key generator that returns a constant value).

## [5.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v5.5.1)

### Added

- The middleware ~throws~ logs an error if `request.ip` is undefined.

### Removed

- Removes typescript typings. (See
[#138](https://github.com/nfriedly/express-rate-limit/issues/138))

## [4.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v4.0.4)

### Changed

- The library no longer modifies the passed-in options object, it instead makes
a clone of it.

## [3.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v3.5.2)

### Added

- Simplifies the default `handler` function so that it no longer changes the
response format. The default handler also uses
[response.send](https://expressjs.com/en/4x/api.html#response.send).

### Changes

- `onLimitReached` now only triggers once for a client and window. However, the
`handle` method is called for every blocked request.

### Removed

- The `delayAfter` and `delayMs` options; they were moved to the
[express-slow-down](https://npmjs.org/package/express-slow-down) package.

## [2.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v2.14.2)

### Added

- A `limiter.resetKey()` method to reset the hit counter for a particular client

### Changes

- The rate limiter now uses a less precise but less resource intensive method of
tracking hits from a client.

### Removed

- The `global` option.
4 changes: 4 additions & 0 deletions config/husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm run pre-commit
222 changes: 222 additions & 0 deletions contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# Contributing Guide

Thanks for your interest in contributing to `express-rate-limit`! This guide
will show you how to set up your environment and contribute to this library.

## Set Up

First, you need to install and be familiar the following:

- `git`: [Here](https://github.com/git-guides) is a great guide by GitHub on
installing and getting started with Git.
- `node` and `npm`:
[This guide](https://nodejs.org/en/download/package-manager/) will help you
install Node and npm. The recommended method is using the `n` version manager
if you are on MacOS or Linux. Make sure you are using the
[active LTS version](https://github.com/nodejs/Release#release-schedule) of
Node.

Once you have installed the above, follow
[these instructions](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
to
[`fork`](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks)
and [`clone`](https://github.com/git-guides/git-clone) the repository
(`nfriedly/express-rate-limit`).

Once you have forked and cloned the repository, you can
[pick out an issue](https://github.com/nfriedly/express-rate-limit/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
you want to fix/implement!

## Making Changes

Once you have cloned the repository to your computer (say, in
`~/Code/express-rate-limit`) and picked the issue you want to tackle, create a
branch:

```sh
> git checkout -b branch-name
```

While naming your branch, try to follow the below guidelines:

1. Prefix the branch name with the type of change being made:
- `fix`: For a bug fix.
- `feat`: For a new feature.
- `test`: For any change related to tests.
- `perf`: For a performance related change.
- `meta`: Anything related to the build process, workflows, issue templates,
etc.
- `refc`: For any refactoring work.
- `docs`: For any documentation related changes.
2. Make the branch name short but self-explanatory.

Once you have created a branch, you can start coding!

The library is written in
[Typescript](https://github.com/microsoft/TypeScript#readme) and
[supports all LTS versions](https://github.com/nodejs/Release#release-schedule)
of Node. The code is arranged as follows:

```sh
express-rate-limit
├── config/
│ └── husky/
│ ├── _
│ └── pre-commit
├── source/
│ ├── index.ts
│ ├── lib.ts
│ ├── memory-store.ts
│ └── types.ts
├── test/
│ ├── external/
│ │ ├── imports/
│ │ ├── stores/
│ │ └── run-all-tests
│ └── library/
│ ├── helpers/
│ │ └── create-server.ts
│ ├── headers-test.ts
│ ├── memory-store-test.ts
│ ├── middleware-test.ts
│ └── options-test.ts
├── changelog.md
├── contributing.md
├── license.md
├── package-lock.json
├── package.json
├── readme.md
└── tsconfig.json
```

> Most files have a little description of what they do at the top.
#### `./`

- `package.json`: Node package information.
- `package-lock.json`: npm lock file, please do not modify manually.
- `tsconfig.json`: The TSC configuration for this project.
- `changelog.md`: A list of changes that have been made in each version.
- `contributing.md`: This file, helps contributors get started.
- `license.md`: Tells people how they can use this package.
- `readme.md`: The file everyone should read before using the package. Contains
installation and usage instructions and the API reference.

#### `source/`

- `source/index.ts`: Exports the `rateLimit` function as the default export from
`source/lib.ts`, and types from `source/types.ts`.
- `source/lib.ts`: The option parser and rate limiting middleware.
- `source/types.ts`: Typescript types for the library.
- `source/memory-store.ts`: The default, built-in memory store for the rate
limiter.

#### `test/library/`

- `test/library/helpers/create-server.ts`: Helper function to create an Express
server and register the middleware passed to it.
- `test/library/options-test.ts`: Ensures the library can parse options
correctly.
- `test/library/headers-test.ts`: Ensures that the middleware returns the
correct headers.
- `test/library/middleware-test.ts`: Ensures the middleware works correctly with
in various different situations.
- `test/library/memory-store-test.ts`: Tests the default, built-in memory store.

#### `test/external/`

- `test/external/imports/*`: Ensures the library can be imported in several
different environments (`js-cjs`, `js-esm`, `ts-cjs`, `ts-esm`).
- `test/external/stores/*`: Ensures the library works with several external
stores (`redis`, `mongo`, `memcached`, `precise`).
- `test/external/run-all-tests`: Sets up and then runs all external tests.

#### `config/`

- `config/husky/pre-commit`: The bash script to run just before someone runs
`git commit`.

When adding a new feature/fixing a bug, please add/update the readme and
changelog as well as add tests for the same. Also make sure your code has been
linted and that existing tests pass. You can run the linter using `npm lint`,
the tests using `npm test` and try to automatically fix most lint issues using
`npm autofix`.

Once you have made changes to the code, you will want to
[`commit`](https://github.com/git-guides/git-commit) (basically, Git's version
of save) the changes. To commit the changes you have made locally:

```sh
> git add this/folder that/file
> git commit --message 'commit-message'
```

While writing the `commit-message`, try to follow the below guidelines:

1. Prefix the message with `type:`, where `type` is one of the following
dependending on what the commit does:
- `fix`: Introduces a bug fix.
- `feat`: Adds a new feature.
- `test`: Any change related to tests.
- `perf`: Any performance related change.
- `meta`: Any change related to the build process, workflows, issue
templates, etc.
- `refc`: Any refactoring work.
- `docs`: Any documentation related changes.
2. Keep the first line brief, and less than 60 characters.
3. Try describing the change in detail in a new paragraph (double newline after
the first line).

When you commit files, `husky` and `lint-staged` will automatically lint the
code and fix most issues. In case an error is not automatically fixable, they
will cancel the commit. Please fix the errors before committing the changes.

## Contributing Changes

Once you have committed your changes, you will want to
[`push`](https://github.com/git-guides/git-push) (basically, publish your
changes to GitHub) your commits. To push your changes to your fork:

```sh
> git push origin branch-name
```

If there are changes made to the `main` branch of the
`nfriedly/express-rate-limit` repository, you may wish to
[`rebase`](https://docs.github.com/en/get-started/using-git/about-git-rebase)
your branch to include those changes. To rebase, or include the changes from the
`main` branch of the `nfriedly/express-rate-limit` repository:

```
> git fetch upstream main
> git rebase upstream/main
```

This will automatically add the changes from `main` branch of the
`nfriedly/express-rate-limit` repository to the current branch. If you encounter
any merge conflicts, follow
[this guide](https://docs.github.com/en/get-started/using-git/resolving-merge-conflicts-after-a-git-rebase)
to resolve them.

Once you have pushed your changes to your fork, follow
[these instructions](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
to open a
[`pull request`](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests):

Once you have submitted a pull request, the maintainers of the repository will
review your pull requests. Whenever a maintainer reviews a pull request they may
request changes. These may be small, such as fixing a typo, or may involve
substantive changes. Such requests are intended to be helpful, but at times may
come across as abrupt or unhelpful, especially if they do not include concrete
suggestions on how to change them. Try not to be discouraged. If you feel that a
review is unfair, say so or seek the input of another project contributor. Often
such comments are the result of a reviewer having taken insufficient time to
review and are not ill-intended. Such difficulties can often be resolved with a
bit of patience. That said, reviewers should be expected to provide helpful
feedback.

In order to land, a pull request needs to be reviewed and approved by at least
one maintainer and pass CI. After that, if there are no objections from other
contributors, the pull request can be merged.

#### Congratulations and thanks for your contribution!
190 changes: 0 additions & 190 deletions lib/express-rate-limit.js

This file was deleted.

47 changes: 0 additions & 47 deletions lib/memory-store.js

This file was deleted.

20 changes: 20 additions & 0 deletions license.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# MIT License

Copyright 2021 Nathan Friedly

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21,634 changes: 19,133 additions & 2,501 deletions package-lock.json

Large diffs are not rendered by default.

193 changes: 142 additions & 51 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,53 +1,144 @@
{
"name": "express-rate-limit",
"version": "5.4.1",
"description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.",
"homepage": "https://github.com/nfriedly/express-rate-limit",
"author": {
"name": "Nathan Friedly",
"url": "http://nfriedly.com/"
},
"repository": "nfriedly/express-rate-limit",
"license": "MIT",
"main": "lib/express-rate-limit.js",
"files": [
"lib/"
],
"keywords": [
"express-rate-limit",
"express",
"rate",
"limit",
"ratelimit",
"rate-limit",
"middleware",
"ip",
"auth",
"authorization",
"security",
"brute",
"force",
"bruteforce",
"brute-force",
"attack"
],
"devDependencies": {
"bluebird": "^3.7.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"express": "^4.17.1",
"husky": "^7.0.2",
"mocha": "^9.1.2",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.1",
"sinon": "^11.1.2",
"supertest": "^6.1.6"
},
"scripts": {
"lint": "eslint .",
"autofix": "npm run lint -- --fix",
"test": "npm run lint && mocha",
"precommit": "pretty-quick --staged"
}
"name": "express-rate-limit",
"version": "6.2.0",
"description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.",
"author": {
"name": "Nathan Friedly",
"url": "http://nfriedly.com/"
},
"license": "MIT",
"homepage": "https://github.com/nfriedly/express-rate-limit",
"repository": "https://github.com/nfriedly/express-rate-limit",
"keywords": [
"express-rate-limit",
"express",
"rate",
"limit",
"ratelimit",
"rate-limit",
"middleware",
"ip",
"auth",
"authorization",
"security",
"brute",
"force",
"bruteforce",
"brute-force",
"attack"
],
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist/",
"tsconfig.json",
"package.json",
"readme.md",
"license.md",
"changelog.md"
],
"engines": {
"node": ">= 14.5.0"
},
"scripts": {
"clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
"build:cjs": "esbuild --bundle --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;\" source/index.ts",
"build:esm": "esbuild --bundle --format=esm --outfile=dist/index.mjs source/index.ts",
"build:types": "dts-bundle-generator --out-file=dist/index.d.ts source/index.ts",
"compile": "run-s clean build:*",
"lint:code": "xo --ignore test/external/",
"lint:rest": "prettier --ignore-path .gitignore --ignore-unknown --check .",
"lint": "run-s lint:*",
"autofix:code": "xo --ignore test/external/ --fix",
"autofix:rest": "prettier --ignore-path .gitignore --ignore-unknown --write .",
"autofix": "run-s autofix:*",
"test:lib": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
"test:ext": "cd test/external/ && bash run-all-tests",
"test": "run-s lint test:*",
"pre-commit": "lint-staged",
"prepare": "run-s compile && husky install config/husky"
},
"peerDependencies": {
"express": "^4"
},
"devDependencies": {
"@jest/globals": "^27.4.6",
"@types/express": "^4.17.13",
"@types/jest": "^27.4.0",
"@types/node": "^16.11.21",
"@types/supertest": "^2.0.11",
"cross-env": "^7.0.3",
"del-cli": "^4.0.1",
"dts-bundle-generator": "^6.4.0",
"esbuild": "^0.14.12",
"express": "^4.17.1",
"husky": "^7.0.4",
"jest": "^27.4.7",
"lint-staged": "^12.2.2",
"npm-run-all": "^4.1.5",
"supertest": "^6.2.2",
"ts-jest": "^27.1.3",
"ts-node": "^10.4.0",
"typescript": "^4.5.5",
"xo": "^0.47.0"
},
"xo": {
"prettier": true,
"rules": {
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-dynamic-delete": 0,
"@typescript-eslint/no-confusing-void-expression": 0,
"@typescript-eslint/consistent-indexed-object-style": [
"error",
"index-signature"
]
}
},
"prettier": {
"semi": false,
"useTabs": true,
"singleQuote": true,
"bracketSpacing": true,
"trailingComma": "all",
"proseWrap": "always"
},
"jest": {
"preset": "ts-jest/presets/default-esm",
"globals": {
"ts-jest": {
"useESM": true
}
},
"verbose": true,
"collectCoverage": true,
"collectCoverageFrom": [
"source/**/*.ts"
],
"testTimeout": 30000,
"testMatch": [
"**/test/library/**/*-test.[jt]s?(x)"
],
"moduleFileExtensions": [
"js",
"jsx",
"json",
"ts",
"tsx"
],
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
}
},
"lint-staged": {
"{source,test}/**/*.ts": "xo --ignore test/external/ --fix",
"**/*.{json,yaml,md}": "prettier --ignore-path .gitignore --ignore-unknown --write "
}
}
473 changes: 473 additions & 0 deletions readme.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// /source/index.ts
// Export away!

// Export all the types as named exports
export * from './types.js'

// Export the rateLimit function as a default export and as a named export, if
// the default export does not work (see https://github.com/nfriedly/express-rate-limit/issues/280)
export { default, default as rateLimit } from './lib.js'

// Export the memory store in case someone wants to use or extend it
// (see https://github.com/nfriedly/express-rate-limit/issues/289)
export { default as MemoryStore } from './memory-store.js'
331 changes: 331 additions & 0 deletions source/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
// /source/lib.ts
// The option parser and rate limiting middleware

import { Request, Response, NextFunction, RequestHandler } from 'express'

import MemoryStore from './memory-store.js'
import {
Options,
AugmentedRequest,
RateLimitRequestHandler,
LegacyStore,
Store,
IncrementResponse,
} from './types.js'

/**
* Type guard to check if a store is legacy store.
*
* @param store {LegacyStore | Store} - The store to check.
*
* @return {boolean} - Whether the store is a legacy store.
*/
const isLegacyStore = (store: LegacyStore | Store): store is LegacyStore =>
// Check that `incr` exists but `increment` does not - store authors might want
// to keep both around for backwards compatibility.
typeof (store as any).incr === 'function' &&
typeof (store as any).increment !== 'function'

/**
* Converts a legacy store to the promisified version.
*
* @param store {LegacyStore | Store} - The store passed to the middleware.
*
* @returns {Store} - The promisified version of the store.
*/
const promisifyStore = (passedStore: LegacyStore | Store): Store => {
if (!isLegacyStore(passedStore)) {
// It's not an old store, return as is
return passedStore
}

const legacyStore = passedStore

// A promisified version of the store
class PromisifiedStore implements Store {
async increment(key: string): Promise<IncrementResponse> {
return new Promise((resolve, reject) => {
legacyStore.incr(
key,
(
error: Error | undefined,
totalHits: number,
resetTime: Date | undefined,
) => {
if (error) reject(error)
resolve({ totalHits, resetTime })
},
)
})
}

async decrement(key: string): Promise<void> {
return Promise.resolve(legacyStore.decrement(key))
}

async resetKey(key: string): Promise<void> {
return Promise.resolve(legacyStore.resetKey(key))
}

async resetAll(): Promise<void> {
if (typeof legacyStore.resetAll === 'function')
return Promise.resolve(legacyStore.resetAll())
}
}

return new PromisifiedStore()
}

/**
* Type-checks and adds the defaults for options the user has not specified.
*
* @param options {Options} - The options the user specifies.
*
* @returns {Options} - A complete configuration object.
*/
const parseOptions = (
passedOptions: Omit<Partial<Options>, 'store'> & {
store?: Store | LegacyStore
},
): Options => {
// See ./types.ts#Options for a detailed description of the options and their
// defaults.
const options = {
windowMs: 60 * 1000,
store: new MemoryStore(),
max: 5,
message: 'Too many requests, please try again later.',
statusCode: 429,
legacyHeaders: passedOptions.headers ?? true,
standardHeaders: passedOptions.draft_polli_ratelimit_headers ?? false,
requestPropertyName: 'rateLimit',
skipFailedRequests: false,
skipSuccessfulRequests: false,
requestWasSuccessful: (_request: Request, response: Response): boolean =>
response.statusCode < 400,
skip: (_request: Request, _response: Response): boolean => false,
keyGenerator: (request: Request, _response: Response): string => {
if (!request.ip) {
console.error(
'WARN | `express-rate-limit` | `request.ip` is undefined. You can avoid this by providing a custom `keyGenerator` function, but it may be indicative of a larger issue.',
)
}

return request.ip
},
handler: (
_request: Request,
response: Response,
_next: NextFunction,
_optionsUsed: Options,
): void => {
response.status(options.statusCode).send(options.message)
},
onLimitReached: (
_request: Request,
_response: Response,
_optionsUsed: Options,
): void => {},
// Allow the above to be overriden by the options passed to the middleware
...passedOptions,
}

// Ensure that the store passed implements the either the `Store` or `LegacyStore`
// interface
if (
(typeof (options.store as LegacyStore).incr !== 'function' &&
typeof (options.store as Store).increment !== 'function') ||
typeof options.store.decrement !== 'function' ||
typeof options.store.resetKey !== 'function' ||
(typeof options.store.resetAll !== 'undefined' &&
typeof options.store.resetAll !== 'function') ||
(typeof (options.store as Store).init !== 'undefined' &&
typeof (options.store as Store).init !== 'function')
) {
throw new TypeError(
'An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface.',
)
}

// Promisify the store, if it is not already
options.store = promisifyStore(options.store)

// Return the 'clean' options
return options as Options
}

/**
* Just pass on any errors for the developer to handle, usually as a HTTP 500
* Internal Server Error.
*
* @param fn {RequestHandler} - The request handler for which to handle errors.
*
* @returns {RequestHandler} - The request handler wrapped with a `.catch` clause.
*
* @private
*/
const handleAsyncErrors =
(fn: RequestHandler): RequestHandler =>
async (request: Request, response: Response, next: NextFunction) => {
try {
await Promise.resolve(fn(request, response, next)).catch(next)
} catch (error: unknown) {
next(error)
}
}

/**
*
* Create an instance of IP rate-limiting middleware for Express.
*
* @param passedOptions {Options} - Options to configure the rate limiter.
*
* @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration.
*
* @public
*/
const rateLimit = (
passedOptions?: Omit<Partial<Options>, 'store'> & {
store?: Store | LegacyStore
},
): RateLimitRequestHandler => {
// Parse the options and add the default values for unspecified options
const options = parseOptions(passedOptions ?? {})
// Call the `init` method on the store, if it exists
if (typeof options.store.init === 'function') options.store.init(options)

// Then return the actual middleware
const middleware = handleAsyncErrors(
async (request: Request, response: Response, next: NextFunction) => {
// First check if we should skip the request
const skip = await options.skip(request, response)
if (skip) {
next()
return
}

// Create an augmented request
const augmentedRequest = request as AugmentedRequest

// Get a unique key for the client
const key = await options.keyGenerator(request, response)
// Increment the client's hit counter by one
const { totalHits, resetTime } = await options.store.increment(key)

// Get the quota (max number of hits) for each client
const retrieveQuota =
typeof options.max === 'function'
? options.max(request, response)
: options.max

const maxHits = await retrieveQuota
// Set the rate limit information on the augmented request object
augmentedRequest[options.requestPropertyName] = {
limit: maxHits,
current: totalHits,
remaining: Math.max(maxHits - totalHits, 0),
resetTime,
}

// Set the X-RateLimit headers on the response object if enabled
if (options.legacyHeaders && !response.headersSent) {
response.setHeader('X-RateLimit-Limit', maxHits)
response.setHeader(
'X-RateLimit-Remaining',
augmentedRequest[options.requestPropertyName].remaining,
)

// If we have a resetTime, also provide the current date to help avoid issues with incorrect clocks
if (resetTime instanceof Date) {
response.setHeader('Date', new Date().toUTCString())
response.setHeader(
'X-RateLimit-Reset',
Math.ceil(resetTime.getTime() / 1000),
)
}
}

// Set the standardized RateLimit headers on the response object
// if enabled
if (options.standardHeaders && !response.headersSent) {
response.setHeader('RateLimit-Limit', maxHits)
response.setHeader(
'RateLimit-Remaining',
augmentedRequest[options.requestPropertyName].remaining,
)

if (resetTime) {
const deltaSeconds = Math.ceil(
(resetTime.getTime() - Date.now()) / 1000,
)
response.setHeader('RateLimit-Reset', Math.max(0, deltaSeconds))
}
}

// If we are to skip failed/successfull requests, decrement the
// counter accordingly once we know the status code of the request
if (options.skipFailedRequests || options.skipSuccessfulRequests) {
let decremented = false
const decrementKey = async () => {
if (!decremented) {
await options.store.decrement(key)
decremented = true
}
}

if (options.skipFailedRequests) {
response.on('finish', async () => {
if (!options.requestWasSuccessful(request, response))
await decrementKey()
})
response.on('close', async () => {
if (!response.writableEnded) await decrementKey()
})
response.on('error', async () => {
await decrementKey()
})
}

if (options.skipSuccessfulRequests) {
response.on('finish', async () => {
if (options.requestWasSuccessful(request, response))
await decrementKey()
})
}
}

// Call the `onLimitReached` callback on the first request where client
// exceeds their rate limit
// NOTE: `onLimitReached` is deprecated, this should be removed in v7.x
if (maxHits && totalHits === maxHits + 1) {
options.onLimitReached(request, response, options)
}

// If the client has exceeded their rate limit, set the Retry-After header
// and call the `handler` function
if (maxHits && totalHits > maxHits) {
if (
(options.legacyHeaders || options.standardHeaders) &&
!response.headersSent
) {
response.setHeader('Retry-After', Math.ceil(options.windowMs / 1000))
}

options.handler(request, response, next, options)
return
}

next()
},
)

// Export the store's function to reset the hit counter for a particular
// client based on their identifier
;(middleware as RateLimitRequestHandler).resetKey =
options.store.resetKey.bind(options.store)

return middleware as RateLimitRequestHandler
}

// Export it to the world!
export default rateLimit
121 changes: 121 additions & 0 deletions source/memory-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// /source/memory-store.ts
// A memory store for hit counts

import { Store, Options, IncrementResponse } from './types.js'

/**
* Calculates the time when all hit counters will be reset.
*
* @param windowMs {number} - The duration of a window (in milliseconds).
*
* @returns {Date}
*
* @private
*/
const calculateNextResetTime = (windowMs: number): Date => {
const resetTime = new Date()
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs)
return resetTime
}

/**
* A `Store` that stores the hit count for each client in memory.
*
* @public
*/
export default class MemoryStore implements Store {
/**
* The duration of time before which all hit counts are reset (in milliseconds).
*/
windowMs!: number

/**
* The map that stores the number of hits for each client in memory.
*/
hits!: {
[key: string]: number | undefined
}

/**
* The time at which all hit counts will be reset.
*/
resetTime!: Date

/**
* Method that initializes the store.
*
* @param options {Options} - The options used to setup the middleware.
*/
init(options: Options): void {
// Get the duration of a window from the options
this.windowMs = options.windowMs
// Then calculate the reset time using that
this.resetTime = calculateNextResetTime(this.windowMs)

// Initialise the hit counter map
this.hits = {}

// Reset hit counts for ALL clients every `windowMs` - this will also
// re-calculate the `resetTime`
const interval = setInterval(async () => {
await this.resetAll()
}, this.windowMs)
if (interval.unref) {
interval.unref()
}
}

/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @returns {IncrementResponse} - The number of hits and reset time for that client.
*
* @public
*/
async increment(key: string): Promise<IncrementResponse> {
const totalHits = (this.hits[key] ?? 0) + 1
this.hits[key] = totalHits

return {
totalHits,
resetTime: this.resetTime,
}
}

/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
async decrement(key: string): Promise<void> {
const current = this.hits[key]
if (current) {
this.hits[key] = current - 1
}
}

/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
async resetKey(key: string): Promise<void> {
delete this.hits[key]
}

/**
* Method to reset everyone's hit counter.
*
* @public
*/
async resetAll(): Promise<void> {
this.hits = {}
this.resetTime = calculateNextResetTime(this.windowMs)
}
}
318 changes: 318 additions & 0 deletions source/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
// /source/types.ts
// All the types used by this package

import { Request, Response, NextFunction, RequestHandler } from 'express'

/**
* Callback that fires when a client's hit counter is incremented.
*
* @param error {Error | undefined} - The error that occurred, if any.
* @param totalHits {number} - The number of hits for that client so far.
* @param resetTime {Date | undefined} - The time when the counter resets.
*/
export type IncrementCallback = (
error: Error | undefined,
totalHits: number,
resetTime: Date | undefined,
) => void

/**
* Method (in the form of middleware) to generate/retrieve a value based on the
* incoming request.
*
* @param request {Request} - The Express request object.
* @param response {Response} - The Express response object.
*
* @returns {T} - The value needed.
*/
export type ValueDeterminingMiddleware<T> = (
request: Request,
response: Response,
) => T | Promise<T>

/**
* Express request handler that sends back a response when a client is
* rate-limited.
*
* @param request {Request} - The Express request object.
* @param response {Response} - The Express response object.
* @param next {NextFunction} - The Express `next` function, can be called to skip responding.
* @param optionsUsed {Options} - The options used to set up the middleware.
*/
export type RateLimitExceededEventHandler = (
request: Request,
response: Response,
next: NextFunction,
optionsUsed: Options,
) => void

/**
* Event callback that is triggered on a client's first request that exceeds the limit
* but not for subsequent requests. May be used for logging, etc. Should *not*
* send a response.
*
* @param request {Request} - The Express request object.
* @param response {Response} - The Express response object.
* @param optionsUsed {Options} - The options used to set up the middleware.
*/
export type RateLimitReachedEventHandler = (
request: Request,
response: Response,
optionsUsed: Options,
) => void

/**
* Data returned from the `Store` when a client's hit counter is incremented.
*
* @property totalHits {number} - The number of hits for that client so far.
* @property resetTime {Date | undefined} - The time when the counter resets.
*/
export type IncrementResponse = {
totalHits: number
resetTime: Date | undefined
}

/**
* A modified Express request handler with the rate limit functions.
*/
export type RateLimitRequestHandler = RequestHandler & {
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*/
resetKey: (key: string) => void
}

/**
* An interface that all hit counter stores must implement.
*
* @deprecated 6.x - Implement the `Store` interface instead.
*/
export interface LegacyStore {
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client.
* @param callback {IncrementCallback} - The callback to call once the counter is incremented.
*/
incr: (key: string, callback: IncrementCallback) => void

/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client.
*/
decrement: (key: string) => void

/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*/
resetKey: (key: string) => void

/**
* Method to reset everyone's hit counter.
*/
resetAll?: () => void
}

/**
* An interface that all hit counter stores must implement.
*/
export interface Store {
/**
* Method that initializes the store, and has access to the options passed to
* the middleware too.
*
* @param options {Options} - The options used to setup the middleware.
*/
init?: (options: Options) => void

/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @returns {IncrementResponse} - The number of hits and reset time for that client.
*/
increment: (key: string) => Promise<IncrementResponse> | IncrementResponse

/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client.
*/
decrement: (key: string) => Promise<void> | void

/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*/
resetKey: (key: string) => Promise<void> | void

/**
* Method to reset everyone's hit counter.
*/
resetAll?: () => Promise<void> | void
}

/**
* The configuration options for the rate limiter.
*/
export interface Options {
/**
* How long we should remember the requests.
*
* Defaults to `60000` ms (= 1 minute).
*/
readonly windowMs: number

/**
* The maximum number of connections to allow during the `window` before
* rate limiting the client.
*
* Can be the limit itself as a number or express middleware that parses
* the request and then figures out the limit.
*
* Defaults to `5`.
*/
readonly max: number | ValueDeterminingMiddleware<number>

/**
* The response body to send back when a client is rate limited.
*
* Defaults to `'Too many requests, please try again later.'`
*/
readonly message: any

/**
* The HTTP status code to send back when a client is rate limited.
*
* Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
*/
readonly statusCode: number

/**
* Whether to send `X-RateLimit-*` headers with the rate limit and the number
* of requests.
*
* Defaults to `true` (for backward compatibility).
*/
readonly legacyHeaders: boolean

/**
* Whether to enable support for the standardized rate limit headers (`RateLimit-*`).
*
* Defaults to `false` (for backward compatibility, but its use is recommended).
*/
readonly standardHeaders: boolean

/**
* The name of the property on the request object to store the rate limit info.
*
* Defaults to `rateLimit`.
*/
readonly requestPropertyName: string

/**
* If `true`, the library will (by default) skip all requests that have a 4XX
* or 5XX status.
*
* Defaults to `false`.
*/
readonly skipFailedRequests: boolean

/**
* If `true`, the library will (by default) skip all requests that have a
* status code less than 400.
*
* Defaults to `false`.
*/
readonly skipSuccessfulRequests: boolean

/**
* Method to generate custom identifiers for clients.
*
* By default, the client's IP address is used.
*/
readonly keyGenerator: ValueDeterminingMiddleware<string>

/**
* Express request handler that sends back a response when a client is
* rate-limited.
*
* By default, sends back the `statusCode` and `message` set via the options.
*/
readonly handler: RateLimitExceededEventHandler

/**
* Express request handler that sends back a response when a client has
* reached their rate limit, and will be rate limited on their next request.
*
* @deprecated 6.x - Please use a custom `handler` that checks the number of
* hits instead.
*/
readonly onLimitReached: RateLimitReachedEventHandler

/**
* Method (in the form of middleware) to determine whether or not this request
* counts towards a client's quota.
*
* By default, skips no requests.
*/
readonly skip: ValueDeterminingMiddleware<boolean>

/**
* Method to determine whether or not the request counts as 'succesful'. Used
* when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
*
* By default, requests with a response status code less than 400 are considered
* successful.
*/
readonly requestWasSuccessful: ValueDeterminingMiddleware<boolean>

/**
* The `Store` to use to store the hit count for each client.
*
* By default, the built-in `MemoryStore` will be used.
*/
store: Store

/**
* Whether to send `X-RateLimit-*` headers with the rate limit and the number
* of requests.
*
* @deprecated 6.x - This option was renamed to `legacyHeaders`.
*/
headers?: boolean

/**
* Whether to send `RateLimit-*` headers with the rate limit and the number
* of requests.
*
* @deprecated 6.x - This option was renamed to `standardHeaders`.
*/
draft_polli_ratelimit_headers?: boolean
}

/**
* The extended request object that includes information about the client's
* rate limit.
*/
export type AugmentedRequest = Request & {
[key: string]: RateLimitInfo
}

/**
* The rate limit related information for each client included in the
* Express request object.
*/
export interface RateLimitInfo {
readonly limit: number
readonly current: number
readonly remaining: number
readonly resetTime: Date | undefined
}
734 changes: 0 additions & 734 deletions test/express-rate-limit-test.js

This file was deleted.

7 changes: 7 additions & 0 deletions test/external/imports/default-import/js-cjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/build
/coverage

*.tmp
*.bak
*.tgz
41 changes: 41 additions & 0 deletions test/external/imports/default-import/js-cjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "express-rate-limit-example-js-cjs",
"version": "1.0.0",
"description": "A minimal example (JS-CJS) of a project using the express-rate-limit package.",
"scripts": {
"start": "node source/index.js",
"lint": "eslint source/**/*.js",
"test": "jest"
},
"dependencies": {
"express": "^4.17.2",
"express-rate-limit": "file:../../../../.."
},
"devDependencies": {
"eslint": "^8.6.0",
"jest": "^27.4.5",
"supertest": "^6.1.6"
},
"eslintConfig": {
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2022
},
"env": {
"es6": true,
"node": true
}
},
"jest": {
"verbose": true,
"collectCoverage": true,
"testTimeout": 30000,
"testMatch": [
"**/test/**/*-test.js"
],
"moduleFileExtensions": [
"js",
"json"
]
}
}
21 changes: 21 additions & 0 deletions test/external/imports/default-import/js-cjs/source/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// /source/app.js
// Create a basic server that uses express-rate-limit to rate limit requests

const createServer = require('express')
const rateLimit = require('express-rate-limit')
const { MemoryStore } = require('express-rate-limit')

const app = createServer()

app.use(
rateLimit({
max: 2,
legacyHeaders: false,
standardHeaders: true,
store: new MemoryStore(),
}),
)

app.get('/', (request, response) => response.send('Hello!'))

module.exports = app
8 changes: 8 additions & 0 deletions test/external/imports/default-import/js-cjs/source/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// /source/index.js
// Run the server

const app = require('./app.js')

app.listen(8080, () =>
console.log('Make a GET request to http://localhost:8080!'),
)
12 changes: 12 additions & 0 deletions test/external/imports/default-import/js-cjs/test/server-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// /test/server-test.js
// Tests the server's rate limiting middleware

const request = require('supertest')

const app = require('../source/app.js')

test('rate limiting middleware', async () => {
await request(app).get('/').expect(200)
await request(app).get('/').expect(200)
await request(app).get('/').expect(429)
})
7 changes: 7 additions & 0 deletions test/external/imports/default-import/js-esm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/build
/coverage

*.tmp
*.bak
*.tgz
44 changes: 44 additions & 0 deletions test/external/imports/default-import/js-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "express-rate-limit-example-js-esm",
"version": "1.0.0",
"description": "A minimal example (JS-ESM) of a project using the express-rate-limit package.",
"type": "module",
"scripts": {
"start": "node source/index.js",
"lint": "eslint source/**/*.js",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
},
"dependencies": {
"express": "^4.17.2",
"express-rate-limit": "file:../../../../.."
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^8.6.0",
"jest": "^27.4.5",
"supertest": "^6.1.6"
},
"eslintConfig": {
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"env": {
"es6": true,
"node": true
}
},
"jest": {
"verbose": true,
"collectCoverage": true,
"testTimeout": 30000,
"testMatch": [
"**/test/**/*-test.js"
],
"moduleFileExtensions": [
"js",
"json"
]
}
}
20 changes: 20 additions & 0 deletions test/external/imports/default-import/js-esm/source/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// /source/app.js
// Create a basic server that uses express-rate-limit to rate limit requests

import createServer from 'express'
import rateLimit, { MemoryStore } from 'express-rate-limit'

const app = createServer()

app.use(
rateLimit({
max: 2,
legacyHeaders: false,
standardHeaders: true,
store: new MemoryStore(),
}),
)

app.get('/', (request, response) => response.send('Hello!'))

export default app
8 changes: 8 additions & 0 deletions test/external/imports/default-import/js-esm/source/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// /source/index.js
// Run the server

import app from './app.js'

app.listen(8080, () =>
console.log('Make a GET request to http://localhost:8080!'),
)
12 changes: 12 additions & 0 deletions test/external/imports/default-import/js-esm/test/server-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// /test/server-test.js
// Tests the server's rate limiting middleware

import request from 'supertest'

import app from '../source/app.js'

test('rate limiting middleware', async () => {
await request(app).get('/').expect(200)
await request(app).get('/').expect(200)
await request(app).get('/').expect(429)
})
7 changes: 7 additions & 0 deletions test/external/imports/default-import/ts-cjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/build
/coverage

*.tmp
*.bak
*.tgz
63 changes: 63 additions & 0 deletions test/external/imports/default-import/ts-cjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "express-rate-limit-example-ts-cjs",
"version": "1.0.0",
"description": "A minimal example (TS-CJS) of a project using the express-rate-limit package.",
"scripts": {
"start": "ts-node source/index.ts",
"lint": "eslint --ext=.ts source/**/*.ts",
"test": "jest"
},
"dependencies": {
"express": "^4.17.2",
"express-rate-limit": "file:../../../../.."
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/jest": "^27.0.3",
"@types/node": "^17.0.4",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"eslint": "^8.6.0",
"jest": "^27.4.5",
"supertest": "^6.1.6",
"ts-jest": "^27.1.2",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 2022
},
"env": {
"es6": true,
"node": true
}
},
"jest": {
"verbose": true,
"preset": "ts-jest/presets/default-esm",
"globals": {
"ts-jest": {
"useESM": true
}
},
"collectCoverage": true,
"testTimeout": 30000,
"testMatch": [
"**/test/**/*-test.[jt]s"
],
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
}
}
}
46 changes: 46 additions & 0 deletions test/external/imports/default-import/ts-cjs/source/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// /source/app.ts
// Create a basic server that uses express-rate-limit to rate limit requests

import createServer from 'express'
import rateLimit, {
MemoryStore,
Store,
IncrementResponse,
} from 'express-rate-limit'

class TestStore implements Store {
hits: Record<string, number> = {}

async increment(key: string): Promise<IncrementResponse> {
if (!this.hits[key]) this.hits[key] = 0
this.hits[key] += 1

return {
totalHits: this.hits[key],
resetTime: undefined,
}
}

async decrement(key: string): Promise<void> {
if (this.hits[key]) this.hits[key]--
}

async resetKey(key: string): Promise<void> {
delete this.hits[key]
}
}

const app = createServer()

app.use(
rateLimit({
max: 2,
legacyHeaders: false,
standardHeaders: true,
store: Math.floor(Math.random() * 2) ? new TestStore() : new MemoryStore(),
}),
)

app.get('/', (request, response) => response.send('Hello!'))

export default app
8 changes: 8 additions & 0 deletions test/external/imports/default-import/ts-cjs/source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// /source/index.ts
// Run the server

import app from './app'

app.listen(8080, () =>
console.log('Make a GET request to http://localhost:8080!'),
)
12 changes: 12 additions & 0 deletions test/external/imports/default-import/ts-cjs/test/server-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// /test/server-test.ts
// Tests the server's rate limiting middleware

import { agent as request } from 'supertest'

import app from '../source/app.js'

test('rate limiting middleware', async () => {
await request(app).get('/').expect(200)
await request(app).get('/').expect(200)
await request(app).get('/').expect(429)
})
9 changes: 9 additions & 0 deletions test/external/imports/default-import/ts-cjs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../../../../tsconfig.json",
"include": ["source/"],
"exclude": ["node_modules/"],
"compilerOptions": {
"target": "esnext",
"module": "commonjs"
}
}
7 changes: 7 additions & 0 deletions test/external/imports/default-import/ts-esm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/build
/coverage

*.tmp
*.bak
*.tgz
66 changes: 66 additions & 0 deletions test/external/imports/default-import/ts-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "express-rate-limit-example-ts-esm",
"version": "1.0.0",
"description": "A minimal example (TS-ESM) of a project using the express-rate-limit package.",
"type": "module",
"scripts": {
"start": "node --loader ts-node/esm source/index.ts",
"lint": "eslint --ext=.ts source/**/*.ts",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
},
"dependencies": {
"express": "^4.17.2",
"express-rate-limit": "file:../../../../.."
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/jest": "^27.0.3",
"@types/node": "^17.0.4",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"cross-env": "^7.0.3",
"eslint": "^8.6.0",
"jest": "^27.4.5",
"supertest": "^6.1.6",
"ts-jest": "^27.1.2",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"env": {
"es6": true,
"node": true
}
},
"jest": {
"verbose": true,
"preset": "ts-jest/presets/default-esm",
"globals": {
"ts-jest": {
"useESM": true
}
},
"collectCoverage": true,
"testTimeout": 30000,
"testMatch": [
"**/test/**/*-test.[jt]s"
],
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
}
}
}
46 changes: 46 additions & 0 deletions test/external/imports/default-import/ts-esm/source/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// /source/app.ts
// Create a basic server that uses express-rate-limit to rate limit requests

import createServer from 'express'
import rateLimit, {
MemoryStore,
Store,
IncrementResponse,
} from 'express-rate-limit'

class TestStore implements Store {
hits: Record<string, number> = {}

async increment(key: string): Promise<IncrementResponse> {
if (!this.hits[key]) this.hits[key] = 0
this.hits[key] += 1

return {
totalHits: this.hits[key],
resetTime: undefined,
}
}

async decrement(key: string): Promise<void> {
if (this.hits[key]) this.hits[key]--
}

async resetKey(key: string): Promise<void> {
delete this.hits[key]
}
}

const app = createServer()

app.use(
rateLimit({
max: 2,
legacyHeaders: false,
standardHeaders: true,
store: Math.floor(Math.random() * 2) ? new TestStore() : new MemoryStore(),
}),
)

app.get('/', (request, response) => response.send('Hello!'))

export default app
8 changes: 8 additions & 0 deletions test/external/imports/default-import/ts-esm/source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// /source/index.ts
// Run the server

import app from './app.js'

app.listen(8080, () =>
console.log('Make a GET request to http://localhost:8080!'),
)
12 changes: 12 additions & 0 deletions test/external/imports/default-import/ts-esm/test/server-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// /test/server-test.ts
// Tests the server's rate limiting middleware

import { agent as request } from 'supertest'

import app from '../source/app.js'

test('rate limiting middleware', async () => {
await request(app).get('/').expect(200)
await request(app).get('/').expect(200)
await request(app).get('/').expect(429)
})
9 changes: 9 additions & 0 deletions test/external/imports/default-import/ts-esm/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../../../../tsconfig.json",
"include": ["source/"],
"exclude": ["node_modules/"],
"compilerOptions": {
"target": "esnext",
"module": "esnext"
}
}
7 changes: 7 additions & 0 deletions test/external/imports/named-import/js-cjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/build
/coverage

*.tmp
*.bak
*.tgz
41 changes: 41 additions & 0 deletions test/external/imports/named-import/js-cjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "express-rate-limit-example-js-cjs",
"version": "1.0.0",
"description": "A minimal example (JS-CJS) of a project using the express-rate-limit package.",
"scripts": {
"start": "node source/index.js",
"lint": "eslint source/**/*.js",
"test": "jest"
},
"dependencies": {
"express": "^4.17.2",
"express-rate-limit": "file:../../../../.."
},
"devDependencies": {
"eslint": "^8.6.0",
"jest": "^27.4.5",
"supertest": "^6.1.6"
},
"eslintConfig": {
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2022
},
"env": {
"es6": true,
"node": true
}
},
"jest": {
"verbose": true,
"collectCoverage": true,
"testTimeout": 30000,
"testMatch": [
"**/test/**/*-test.js"
],
"moduleFileExtensions": [
"js",
"json"
]
}
}
20 changes: 20 additions & 0 deletions test/external/imports/named-import/js-cjs/source/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// /source/app.js
// Create a basic server that uses express-rate-limit to rate limit requests

const createServer = require('express')
const { rateLimit, MemoryStore } = require('express-rate-limit')

const app = createServer()

app.use(
rateLimit({
max: 2,
legacyHeaders: false,
standardHeaders: true,
store: new MemoryStore(),
}),
)

app.get('/', (request, response) => response.send('Hello!'))

module.exports = app
8 changes: 8 additions & 0 deletions test/external/imports/named-import/js-cjs/source/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// /source/index.js
// Run the server

const app = require('./app.js')

app.listen(8080, () =>
console.log('Make a GET request to http://localhost:8080!'),
)
12 changes: 12 additions & 0 deletions test/external/imports/named-import/js-cjs/test/server-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// /test/server-test.js
// Tests the server's rate limiting middleware

const request = require('supertest')

const app = require('../source/app.js')

test('rate limiting middleware', async () => {
await request(app).get('/').expect(200)
await request(app).get('/').expect(200)
await request(app).get('/').expect(429)
})
7 changes: 7 additions & 0 deletions test/external/imports/named-import/js-esm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/build
/coverage

*.tmp
*.bak
*.tgz
44 changes: 44 additions & 0 deletions test/external/imports/named-import/js-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "express-rate-limit-example-js-esm",
"version": "1.0.0",
"description": "A minimal example (JS-ESM) of a project using the express-rate-limit package.",
"type": "module",
"scripts": {
"start": "node source/index.js",
"lint": "eslint source/**/*.js",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
},
"dependencies": {
"express": "^4.17.2",
"express-rate-limit": "file:../../../../.."
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^8.6.0",
"jest": "^27.4.5",
"supertest": "^6.1.6"
},
"eslintConfig": {
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"env": {
"es6": true,
"node": true
}
},
"jest": {
"verbose": true,
"collectCoverage": true,
"testTimeout": 30000,
"testMatch": [
"**/test/**/*-test.js"
],
"moduleFileExtensions": [
"js",
"json"
]
}
}
20 changes: 20 additions & 0 deletions test/external/imports/named-import/js-esm/source/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// /source/app.js
// Create a basic server that uses express-rate-limit to rate limit requests

import createServer from 'express'
import { rateLimit, MemoryStore } from 'express-rate-limit'

const app = createServer()

app.use(
rateLimit({
max: 2,
legacyHeaders: false,
standardHeaders: true,
store: new MemoryStore(),
}),
)

app.get('/', (request, response) => response.send('Hello!'))

export default app
8 changes: 8 additions & 0 deletions test/external/imports/named-import/js-esm/source/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// /source/index.js
// Run the server

import app from './app.js'

app.listen(8080, () =>
console.log('Make a GET request to http://localhost:8080!'),
)
12 changes: 12 additions & 0 deletions test/external/imports/named-import/js-esm/test/server-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// /test/server-test.js
// Tests the server's rate limiting middleware

import request from 'supertest'

import app from '../source/app.js'

test('rate limiting middleware', async () => {
await request(app).get('/').expect(200)
await request(app).get('/').expect(200)
await request(app).get('/').expect(429)
})
7 changes: 7 additions & 0 deletions test/external/imports/named-import/ts-cjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/build
/coverage

*.tmp
*.bak
*.tgz
63 changes: 63 additions & 0 deletions test/external/imports/named-import/ts-cjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "express-rate-limit-example-ts-cjs",
"version": "1.0.0",
"description": "A minimal example (TS-CJS) of a project using the express-rate-limit package.",
"scripts": {
"start": "ts-node source/index.ts",
"lint": "eslint --ext=.ts source/**/*.ts",
"test": "jest"
},
"dependencies": {
"express": "^4.17.2",
"express-rate-limit": "file:../../../../.."
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/jest": "^27.0.3",
"@types/node": "^17.0.4",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"eslint": "^8.6.0",
"jest": "^27.4.5",
"supertest": "^6.1.6",
"ts-jest": "^27.1.2",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 2022
},
"env": {
"es6": true,
"node": true
}
},
"jest": {
"verbose": true,
"preset": "ts-jest/presets/default-esm",
"globals": {
"ts-jest": {
"useESM": true
}
},
"collectCoverage": true,
"testTimeout": 30000,
"testMatch": [
"**/test/**/*-test.[jt]s"
],
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
}
}
}
47 changes: 47 additions & 0 deletions test/external/imports/named-import/ts-cjs/source/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// /source/app.ts
// Create a basic server that uses express-rate-limit to rate limit requests

import createServer from 'express'
import {
rateLimit,
MemoryStore,
Store,
IncrementResponse,
} from 'express-rate-limit'

class TestStore implements Store {
hits: Record<string, number> = {}

async increment(key: string): Promise<IncrementResponse> {
if (!this.hits[key]) this.hits[key] = 0
this.hits[key] += 1

return {
totalHits: this.hits[key],
resetTime: undefined,
}
}

async decrement(key: string): Promise<void> {
if (this.hits[key]) this.hits[key]--
}

async resetKey(key: string): Promise<void> {
delete this.hits[key]
}
}

const app = createServer()

app.use(
rateLimit({
max: 2,
legacyHeaders: false,
standardHeaders: true,
store: Math.floor(Math.random() * 2) ? new TestStore() : new MemoryStore(),
}),
)

app.get('/', (request, response) => response.send('Hello!'))

export default app
8 changes: 8 additions & 0 deletions test/external/imports/named-import/ts-cjs/source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// /source/index.ts
// Run the server

import app from './app'

app.listen(8080, () =>
console.log('Make a GET request to http://localhost:8080!'),
)
12 changes: 12 additions & 0 deletions test/external/imports/named-import/ts-cjs/test/server-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// /test/server-test.ts
// Tests the server's rate limiting middleware

import { agent as request } from 'supertest'

import app from '../source/app.js'

test('rate limiting middleware', async () => {
await request(app).get('/').expect(200)
await request(app).get('/').expect(200)
await request(app).get('/').expect(429)
})
9 changes: 9 additions & 0 deletions test/external/imports/named-import/ts-cjs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../../../../tsconfig.json",
"include": ["source/"],
"exclude": ["node_modules/"],
"compilerOptions": {
"target": "esnext",
"module": "commonjs"
}
}
7 changes: 7 additions & 0 deletions test/external/imports/named-import/ts-esm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/build
/coverage

*.tmp
*.bak
*.tgz
66 changes: 66 additions & 0 deletions test/external/imports/named-import/ts-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "express-rate-limit-example-ts-esm",
"version": "1.0.0",
"description": "A minimal example (TS-ESM) of a project using the express-rate-limit package.",
"type": "module",
"scripts": {
"start": "node --loader ts-node/esm source/index.ts",
"lint": "eslint --ext=.ts source/**/*.ts",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
},
"dependencies": {
"express": "^4.17.2",
"express-rate-limit": "file:../../../../.."
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/jest": "^27.0.3",
"@types/node": "^17.0.4",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"cross-env": "^7.0.3",
"eslint": "^8.6.0",
"jest": "^27.4.5",
"supertest": "^6.1.6",
"ts-jest": "^27.1.2",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"env": {
"es6": true,
"node": true
}
},
"jest": {
"verbose": true,
"preset": "ts-jest/presets/default-esm",
"globals": {
"ts-jest": {
"useESM": true
}
},
"collectCoverage": true,
"testTimeout": 30000,
"testMatch": [
"**/test/**/*-test.[jt]s"
],
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
}
}
}
47 changes: 47 additions & 0 deletions test/external/imports/named-import/ts-esm/source/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// /source/app.ts
// Create a basic server that uses express-rate-limit to rate limit requests

import createServer from 'express'
import {
rateLimit,
MemoryStore,
Store,
IncrementResponse,
} from 'express-rate-limit'

class TestStore implements Store {
hits: Record<string, number> = {}

async increment(key: string): Promise<IncrementResponse> {
if (!this.hits[key]) this.hits[key] = 0
this.hits[key] += 1

return {
totalHits: this.hits[key],
resetTime: undefined,
}
}

async decrement(key: string): Promise<void> {
if (this.hits[key]) this.hits[key]--
}

async resetKey(key: string): Promise<void> {
delete this.hits[key]
}
}

const app = createServer()

app.use(
rateLimit({
max: 2,
legacyHeaders: false,
standardHeaders: true,
store: Math.floor(Math.random() * 2) ? new TestStore() : new MemoryStore(),
}),
)

app.get('/', (request, response) => response.send('Hello!'))

export default app
8 changes: 8 additions & 0 deletions test/external/imports/named-import/ts-esm/source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// /source/index.ts
// Run the server

import app from './app.js'

app.listen(8080, () =>
console.log('Make a GET request to http://localhost:8080!'),
)
Loading