@creator.co/wapi 1.9.3 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@creator.co/wapi",
3
- "version": "1.9.3",
3
+ "version": "1.10.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@creator.co/wapi",
9
- "version": "1.9.3",
9
+ "version": "1.10.1",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@aws-sdk/client-dynamodb": "^3.730.0",
@@ -26,13 +26,13 @@
26
26
  "cuid": "^3.0.0",
27
27
  "dotenv": "^16.4.1",
28
28
  "email-templates": "^13.0.1",
29
- "express": "^4.22.0",
29
+ "express": "^4.22.2",
30
30
  "express-rate-limit": "^7.5.0",
31
31
  "json-stringify-safe": "^5.0.1",
32
32
  "jsonwebtoken": "^9.0.2",
33
33
  "knex": "^3.0.1",
34
34
  "knex-stringcase": "^1.4.6",
35
- "kysely": "^0.28.14",
35
+ "kysely": "^0.28.17",
36
36
  "node-cache": "^5.1.2",
37
37
  "nodemailer": "^8.0.5",
38
38
  "object-hash": "^3.0.0",
@@ -3271,21 +3271,6 @@
3271
3271
  "node": ">=18"
3272
3272
  }
3273
3273
  },
3274
- "node_modules/@ladjs/i18n/node_modules/qs": {
3275
- "version": "6.15.1",
3276
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
3277
- "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
3278
- "license": "BSD-3-Clause",
3279
- "dependencies": {
3280
- "side-channel": "^1.1.0"
3281
- },
3282
- "engines": {
3283
- "node": ">=0.6"
3284
- },
3285
- "funding": {
3286
- "url": "https://github.com/sponsors/ljharb"
3287
- }
3288
- },
3289
3274
  "node_modules/@messageformat/core": {
3290
3275
  "version": "3.4.0",
3291
3276
  "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz",
@@ -6037,22 +6022,23 @@
6037
6022
  ]
6038
6023
  },
6039
6024
  "node_modules/body-parser": {
6040
- "version": "1.20.3",
6041
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
6042
- "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
6025
+ "version": "1.20.5",
6026
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
6027
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
6028
+ "license": "MIT",
6043
6029
  "dependencies": {
6044
- "bytes": "3.1.2",
6030
+ "bytes": "~3.1.2",
6045
6031
  "content-type": "~1.0.5",
6046
6032
  "debug": "2.6.9",
6047
6033
  "depd": "2.0.0",
6048
- "destroy": "1.2.0",
6049
- "http-errors": "2.0.0",
6050
- "iconv-lite": "0.4.24",
6051
- "on-finished": "2.4.1",
6052
- "qs": "6.13.0",
6053
- "raw-body": "2.5.2",
6034
+ "destroy": "~1.2.0",
6035
+ "http-errors": "~2.0.1",
6036
+ "iconv-lite": "~0.4.24",
6037
+ "on-finished": "~2.4.1",
6038
+ "qs": "~6.15.1",
6039
+ "raw-body": "~2.5.3",
6054
6040
  "type-is": "~1.6.18",
6055
- "unpipe": "1.0.0"
6041
+ "unpipe": "~1.0.0"
6056
6042
  },
6057
6043
  "engines": {
6058
6044
  "node": ">= 0.8",
@@ -6063,14 +6049,36 @@
6063
6049
  "version": "2.6.9",
6064
6050
  "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
6065
6051
  "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
6052
+ "license": "MIT",
6066
6053
  "dependencies": {
6067
6054
  "ms": "2.0.0"
6068
6055
  }
6069
6056
  },
6057
+ "node_modules/body-parser/node_modules/http-errors": {
6058
+ "version": "2.0.1",
6059
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
6060
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
6061
+ "license": "MIT",
6062
+ "dependencies": {
6063
+ "depd": "~2.0.0",
6064
+ "inherits": "~2.0.4",
6065
+ "setprototypeof": "~1.2.0",
6066
+ "statuses": "~2.0.2",
6067
+ "toidentifier": "~1.0.1"
6068
+ },
6069
+ "engines": {
6070
+ "node": ">= 0.8"
6071
+ },
6072
+ "funding": {
6073
+ "type": "opencollective",
6074
+ "url": "https://opencollective.com/express"
6075
+ }
6076
+ },
6070
6077
  "node_modules/body-parser/node_modules/iconv-lite": {
6071
6078
  "version": "0.4.24",
6072
6079
  "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
6073
6080
  "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
6081
+ "license": "MIT",
6074
6082
  "dependencies": {
6075
6083
  "safer-buffer": ">= 2.1.2 < 3"
6076
6084
  },
@@ -6081,7 +6089,17 @@
6081
6089
  "node_modules/body-parser/node_modules/ms": {
6082
6090
  "version": "2.0.0",
6083
6091
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
6084
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
6092
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
6093
+ "license": "MIT"
6094
+ },
6095
+ "node_modules/body-parser/node_modules/statuses": {
6096
+ "version": "2.0.2",
6097
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
6098
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
6099
+ "license": "MIT",
6100
+ "engines": {
6101
+ "node": ">= 0.8"
6102
+ }
6085
6103
  },
6086
6104
  "node_modules/boolbase": {
6087
6105
  "version": "1.0.0",
@@ -6196,6 +6214,7 @@
6196
6214
  "version": "3.1.2",
6197
6215
  "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
6198
6216
  "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
6217
+ "license": "MIT",
6199
6218
  "engines": {
6200
6219
  "node": ">= 0.8"
6201
6220
  }
@@ -6600,6 +6619,7 @@
6600
6619
  "version": "1.0.5",
6601
6620
  "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
6602
6621
  "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
6622
+ "license": "MIT",
6603
6623
  "engines": {
6604
6624
  "node": ">= 0.6"
6605
6625
  }
@@ -8310,14 +8330,14 @@
8310
8330
  }
8311
8331
  },
8312
8332
  "node_modules/express": {
8313
- "version": "4.22.1",
8314
- "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
8315
- "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
8333
+ "version": "4.22.2",
8334
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
8335
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
8316
8336
  "license": "MIT",
8317
8337
  "dependencies": {
8318
8338
  "accepts": "~1.3.8",
8319
8339
  "array-flatten": "1.1.1",
8320
- "body-parser": "~1.20.3",
8340
+ "body-parser": "~1.20.5",
8321
8341
  "content-disposition": "~0.5.4",
8322
8342
  "content-type": "~1.0.4",
8323
8343
  "cookie": "~0.7.1",
@@ -8336,7 +8356,7 @@
8336
8356
  "parseurl": "~1.3.3",
8337
8357
  "path-to-regexp": "~0.1.12",
8338
8358
  "proxy-addr": "~2.0.7",
8339
- "qs": "~6.14.0",
8359
+ "qs": "~6.15.1",
8340
8360
  "range-parser": "~1.2.1",
8341
8361
  "safe-buffer": "5.2.1",
8342
8362
  "send": "~0.19.0",
@@ -8388,21 +8408,6 @@
8388
8408
  "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
8389
8409
  "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
8390
8410
  },
8391
- "node_modules/express/node_modules/qs": {
8392
- "version": "6.14.0",
8393
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
8394
- "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
8395
- "license": "BSD-3-Clause",
8396
- "dependencies": {
8397
- "side-channel": "^1.1.0"
8398
- },
8399
- "engines": {
8400
- "node": ">=0.6"
8401
- },
8402
- "funding": {
8403
- "url": "https://github.com/sponsors/ljharb"
8404
- }
8405
- },
8406
8411
  "node_modules/extend-object": {
8407
8412
  "version": "1.0.0",
8408
8413
  "resolved": "https://registry.npmjs.org/extend-object/-/extend-object-1.0.0.tgz",
@@ -11180,9 +11185,9 @@
11180
11185
  "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
11181
11186
  },
11182
11187
  "node_modules/kysely": {
11183
- "version": "0.28.14",
11184
- "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.14.tgz",
11185
- "integrity": "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==",
11188
+ "version": "0.28.17",
11189
+ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz",
11190
+ "integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==",
11186
11191
  "license": "MIT",
11187
11192
  "engines": {
11188
11193
  "node": ">=20.0.0"
@@ -11448,6 +11453,7 @@
11448
11453
  "version": "0.3.0",
11449
11454
  "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
11450
11455
  "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
11456
+ "license": "MIT",
11451
11457
  "engines": {
11452
11458
  "node": ">= 0.6"
11453
11459
  }
@@ -12868,11 +12874,12 @@
12868
12874
  ]
12869
12875
  },
12870
12876
  "node_modules/qs": {
12871
- "version": "6.13.0",
12872
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
12873
- "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
12877
+ "version": "6.15.2",
12878
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
12879
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
12880
+ "license": "BSD-3-Clause",
12874
12881
  "dependencies": {
12875
- "side-channel": "^1.0.6"
12882
+ "side-channel": "^1.1.0"
12876
12883
  },
12877
12884
  "engines": {
12878
12885
  "node": ">=0.6"
@@ -12932,23 +12939,45 @@
12932
12939
  }
12933
12940
  },
12934
12941
  "node_modules/raw-body": {
12935
- "version": "2.5.2",
12936
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
12937
- "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
12942
+ "version": "2.5.3",
12943
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
12944
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
12945
+ "license": "MIT",
12938
12946
  "dependencies": {
12939
- "bytes": "3.1.2",
12940
- "http-errors": "2.0.0",
12941
- "iconv-lite": "0.4.24",
12942
- "unpipe": "1.0.0"
12947
+ "bytes": "~3.1.2",
12948
+ "http-errors": "~2.0.1",
12949
+ "iconv-lite": "~0.4.24",
12950
+ "unpipe": "~1.0.0"
12943
12951
  },
12944
12952
  "engines": {
12945
12953
  "node": ">= 0.8"
12946
12954
  }
12947
12955
  },
12956
+ "node_modules/raw-body/node_modules/http-errors": {
12957
+ "version": "2.0.1",
12958
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
12959
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
12960
+ "license": "MIT",
12961
+ "dependencies": {
12962
+ "depd": "~2.0.0",
12963
+ "inherits": "~2.0.4",
12964
+ "setprototypeof": "~1.2.0",
12965
+ "statuses": "~2.0.2",
12966
+ "toidentifier": "~1.0.1"
12967
+ },
12968
+ "engines": {
12969
+ "node": ">= 0.8"
12970
+ },
12971
+ "funding": {
12972
+ "type": "opencollective",
12973
+ "url": "https://opencollective.com/express"
12974
+ }
12975
+ },
12948
12976
  "node_modules/raw-body/node_modules/iconv-lite": {
12949
12977
  "version": "0.4.24",
12950
12978
  "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
12951
12979
  "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
12980
+ "license": "MIT",
12952
12981
  "dependencies": {
12953
12982
  "safer-buffer": ">= 2.1.2 < 3"
12954
12983
  },
@@ -12956,6 +12985,15 @@
12956
12985
  "node": ">=0.10.0"
12957
12986
  }
12958
12987
  },
12988
+ "node_modules/raw-body/node_modules/statuses": {
12989
+ "version": "2.0.2",
12990
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
12991
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
12992
+ "license": "MIT",
12993
+ "engines": {
12994
+ "node": ">= 0.8"
12995
+ }
12996
+ },
12959
12997
  "node_modules/rc": {
12960
12998
  "version": "1.2.8",
12961
12999
  "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -14550,6 +14588,7 @@
14550
14588
  "version": "1.6.18",
14551
14589
  "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
14552
14590
  "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
14591
+ "license": "MIT",
14553
14592
  "dependencies": {
14554
14593
  "media-typer": "0.3.0",
14555
14594
  "mime-types": "~2.1.24"
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@creator.co/wapi",
3
- "version": "1.9.3",
3
+ "version": "1.10.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -40,13 +40,13 @@
40
40
  "cuid": "^3.0.0",
41
41
  "dotenv": "^16.4.1",
42
42
  "email-templates": "^13.0.1",
43
- "express": "^4.22.0",
43
+ "express": "^4.22.2",
44
44
  "express-rate-limit": "^7.5.0",
45
45
  "json-stringify-safe": "^5.0.1",
46
46
  "jsonwebtoken": "^9.0.2",
47
47
  "knex": "^3.0.1",
48
48
  "knex-stringcase": "^1.4.6",
49
- "kysely": "^0.28.14",
49
+ "kysely": "^0.28.17",
50
50
  "node-cache": "^5.1.2",
51
51
  "nodemailer": "^8.0.5",
52
52
  "object-hash": "^3.0.0",
@@ -30,6 +30,12 @@ export default class Proxy {
30
30
  * @returns None
31
31
  */
32
32
  private readonly serverlessHandler;
33
+ /**
34
+ * Route resolver used to identify per-route rate limit configuration.
35
+ * @private
36
+ * @readonly
37
+ */
38
+ private readonly routeResolver;
33
39
  /**
34
40
  * Represents a listener for an HTTP server.
35
41
  * @private
@@ -70,6 +76,15 @@ export default class Proxy {
70
76
  * @returns None
71
77
  */
72
78
  private installRoutes;
79
+ /**
80
+ * Creates rate limiting middleware from a per-route {@link RateLimitConfig},
81
+ * inheriting the global Redis store configuration when available so all
82
+ * rate-limit counters share the same Redis connection.
83
+ * @param {RateLimitConfig} config - The per-route rate limit configuration
84
+ * @returns {express.RequestHandler} Express middleware for rate limiting
85
+ * @private
86
+ */
87
+ private createRouteRateLimitMiddleware;
73
88
  /**
74
89
  * Creates rate limiting middleware based on the provided configuration.
75
90
  * @param {GlobalRateLimitConfig} config - The rate limit configuration
@@ -18,6 +18,7 @@ import HealthHandler from './HealthHandler.js';
18
18
  import Globals from '../../../Globals.js';
19
19
  import Logger from '../../../Logger/Logger.js';
20
20
  import Utils from '../../../Util/Utils.js';
21
+ import RouteResolver from '../../RouteResolver.js';
21
22
  /* Get package.json version from Wapi on ESM */
22
23
  const { version: appVersion } = JSON.parse(fs.readFileSync('package.json').toString());
23
24
  /**
@@ -34,8 +35,12 @@ export default class Proxy {
34
35
  this.stopping = false;
35
36
  this.config = config;
36
37
  this.serverlessHandler = serverlessHandler;
38
+ this.routeResolver = new RouteResolver(this.config);
37
39
  this.logger = new Logger({ logLevel: 'INFO' }, 'proxy-container');
38
40
  this.app = express();
41
+ // Trust the first proxy hop so req.ip resolves to the real client IP
42
+ // (not the load balancer's IP) when running behind ALB or similar.
43
+ this.app.set('trust proxy', 1);
39
44
  /* Opinionated Express configs */
40
45
  this.app.use(express.json({
41
46
  verify(req, res, buf) {
@@ -54,7 +59,16 @@ export default class Proxy {
54
59
  // Apply global rate limiting if configured
55
60
  if (this.config.rateLimit && this.config.rateLimit.enabled !== false) {
56
61
  this.logger.info('[Proxy] - [RATE-LIMIT] - Global rate limiting enabled');
57
- const rateLimitMiddleware = this.createRateLimitMiddleware(this.config.rateLimit);
62
+ // Augment the skip function to bypass the global limit for routes that
63
+ // have their own rateLimit config (false = disable, or a RateLimitConfig).
64
+ const globalConfig = Object.assign(Object.assign({}, this.config.rateLimit), { skip: (req) => {
65
+ var _a, _b, _c;
66
+ const route = this.routeResolver.resolveRoute(req.method, req.path);
67
+ if (route && route.rateLimit !== undefined)
68
+ return true;
69
+ return (_c = (_b = (_a = this.config.rateLimit).skip) === null || _b === void 0 ? void 0 : _b.call(_a, req)) !== null && _c !== void 0 ? _c : false;
70
+ } });
71
+ const rateLimitMiddleware = this.createRateLimitMiddleware(globalConfig);
58
72
  this.app.use(rateLimitMiddleware);
59
73
  }
60
74
  // //This supposedly fix some 502 codes where nodejs socket would hang during
@@ -148,11 +162,79 @@ export default class Proxy {
148
162
  this.app
149
163
  .route(this.config.healthCheckRoute || Globals.Listener_HTTP_DefaultHealthCheckRoute)
150
164
  .get(HealthHandler);
165
+ // Register individual routes that declare their own rateLimit config.
166
+ // These are installed BEFORE the wildcard so Express matches them first,
167
+ // giving each route an independent rate limit bucket.
168
+ for (const route of this.config.routes) {
169
+ if (!route.rateLimit)
170
+ continue;
171
+ const rlMiddleware = this.createRouteRateLimitMiddleware(route.rateLimit);
172
+ const paths = Array.isArray(route.path) ? route.path : [route.path];
173
+ for (const path of paths) {
174
+ ;
175
+ this.app.route(path)[route.method.toLowerCase()](rlMiddleware, GenericHandler(this.serverlessHandler));
176
+ }
177
+ }
151
178
  //Main route -- We use a wildcard route because is not the job of the runtime and neither
152
179
  //the task to deny/constrain routes that invoked this task; all the job is done by the
153
180
  //load balancer and we just foward everything we have to the function.
154
181
  this.app.route(Globals.Listener_HTTP_ProxyRoute).all(GenericHandler(this.serverlessHandler));
155
182
  }
183
+ /**
184
+ * Creates rate limiting middleware from a per-route {@link RateLimitConfig},
185
+ * inheriting the global Redis store configuration when available so all
186
+ * rate-limit counters share the same Redis connection.
187
+ * @param {RateLimitConfig} config - The per-route rate limit configuration
188
+ * @returns {express.RequestHandler} Express middleware for rate limiting
189
+ * @private
190
+ */
191
+ createRouteRateLimitMiddleware(config) {
192
+ // Resolve keyGenerator string shorthands to concrete functions.
193
+ let keyGenerator;
194
+ if (config.keyGenerator === 'ip') {
195
+ keyGenerator = (req) => req.ip || req.socket.remoteAddress || 'unknown';
196
+ }
197
+ else if (config.keyGenerator === 'userId') {
198
+ keyGenerator = (req) => {
199
+ const authHeader = req.headers['authorization'];
200
+ if (authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith('Bearer ')) {
201
+ const parts = authHeader.slice(7).split('.');
202
+ if (parts.length === 3) {
203
+ try {
204
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
205
+ const userId = payload.sub || payload.id || payload.userId;
206
+ if (userId)
207
+ return String(userId);
208
+ }
209
+ catch (_a) {
210
+ // malformed token — fall through to IP
211
+ }
212
+ }
213
+ }
214
+ return req.ip || req.socket.remoteAddress || 'unknown';
215
+ };
216
+ }
217
+ else {
218
+ keyGenerator = config.keyGenerator;
219
+ }
220
+ // Inherit the global Redis store (if configured) so per-route limiters
221
+ // share the same connection; distinguish them with a unique key prefix.
222
+ const globalRl = this.config.rateLimit;
223
+ const globalConfig = {
224
+ windowMs: config.windowMs,
225
+ limit: config.limit,
226
+ keyGenerator,
227
+ skip: config.skip,
228
+ store: globalRl === null || globalRl === void 0 ? void 0 : globalRl.store,
229
+ redis: (globalRl === null || globalRl === void 0 ? void 0 : globalRl.redis)
230
+ ? {
231
+ client: globalRl.redis.client,
232
+ prefix: `${globalRl.redis.prefix || 'wapi:rl:'}route:`,
233
+ }
234
+ : undefined,
235
+ };
236
+ return this.createRateLimitMiddleware(globalConfig);
237
+ }
156
238
  /**
157
239
  * Creates rate limiting middleware based on the provided configuration.
158
240
  * @param {GlobalRateLimitConfig} config - The rate limit configuration
@@ -169,12 +251,27 @@ export default class Proxy {
169
251
  // Key generator - how to identify unique clients
170
252
  keyGenerator: config.keyGenerator ||
171
253
  ((req) => {
172
- var _a, _b;
173
- // Use IP address from proxy-aware sources
174
- return (req.ip ||
175
- ((_b = (_a = req.headers['x-forwarded-for']) === null || _a === void 0 ? void 0 : _a.split(',')[0]) === null || _b === void 0 ? void 0 : _b.trim()) ||
176
- req.socket.remoteAddress ||
177
- 'unknown');
254
+ // For authenticated requests, use the stable user ID from the JWT
255
+ // payload so that multiple users behind the same IP are bucketed
256
+ // independently and token rotation doesn't change a user's bucket.
257
+ const authHeader = req.headers['authorization'];
258
+ if (authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith('Bearer ')) {
259
+ const parts = authHeader.slice(7).split('.');
260
+ if (parts.length === 3) {
261
+ try {
262
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
263
+ const userId = payload.sub || payload.id || payload.userId;
264
+ if (userId)
265
+ return String(userId);
266
+ }
267
+ catch (_a) {
268
+ // malformed token — fall through to IP
269
+ }
270
+ }
271
+ }
272
+ // Unauthenticated: fall back to real client IP.
273
+ // trust proxy is set so req.ip reflects x-forwarded-for correctly.
274
+ return req.ip || req.socket.remoteAddress || 'unknown';
178
275
  }),
179
276
  // Custom handler when rate limit is exceeded
180
277
  handler: config.handler ||
@@ -1 +1 @@
1
- {"version":3,"file":"Proxy.js","sourceRoot":"","sources":["../../../../../src/Server/lib/container/Proxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Proxy.js","sourceRoot":"","sources":["../../../../../src/Server/lib/container/Proxy.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@creator.co/wapi",
3
- "version": "1.9.3",
3
+ "version": "1.10.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -40,13 +40,13 @@
40
40
  "cuid": "^3.0.0",
41
41
  "dotenv": "^16.4.1",
42
42
  "email-templates": "^13.0.1",
43
- "express": "^4.22.0",
43
+ "express": "^4.22.2",
44
44
  "express-rate-limit": "^7.5.0",
45
45
  "json-stringify-safe": "^5.0.1",
46
46
  "jsonwebtoken": "^9.0.2",
47
47
  "knex": "^3.0.1",
48
48
  "knex-stringcase": "^1.4.6",
49
- "kysely": "^0.28.14",
49
+ "kysely": "^0.28.17",
50
50
  "node-cache": "^5.1.2",
51
51
  "nodemailer": "^8.0.5",
52
52
  "object-hash": "^3.0.0",
@@ -9,10 +9,12 @@ import { RedisStore } from 'rate-limit-redis'
9
9
  import Server from './../Server.js'
10
10
  import GenericHandler from './GenericHandler.js'
11
11
  import HealthHandler from './HealthHandler.js'
12
+ import { HttpMethod } from '../../../API/Request.js'
12
13
  import Globals from '../../../Globals.js'
13
14
  import Logger from '../../../Logger/Logger.js'
14
15
  import Utils from '../../../Util/Utils.js'
15
- import { GlobalRateLimitConfig, RouterConfig } from '../../Router.js'
16
+ import { GlobalRateLimitConfig, RateLimitConfig, RouterConfig } from '../../Router.js'
17
+ import RouteResolver from '../../RouteResolver.js'
16
18
 
17
19
  /* Get package.json version from Wapi on ESM */
18
20
  const { version: appVersion } = JSON.parse(fs.readFileSync('package.json').toString())
@@ -47,6 +49,12 @@ export default class Proxy {
47
49
  * @returns None
48
50
  */
49
51
  private readonly serverlessHandler: Server['handleServerlessEvent']
52
+ /**
53
+ * Route resolver used to identify per-route rate limit configuration.
54
+ * @private
55
+ * @readonly
56
+ */
57
+ private readonly routeResolver: RouteResolver
50
58
  /**
51
59
  * Represents a listener for an HTTP server.
52
60
  * @private
@@ -64,8 +72,12 @@ export default class Proxy {
64
72
  this.stopping = false
65
73
  this.config = config
66
74
  this.serverlessHandler = serverlessHandler
75
+ this.routeResolver = new RouteResolver(this.config)
67
76
  this.logger = new Logger({ logLevel: 'INFO' }, 'proxy-container')
68
77
  this.app = express()
78
+ // Trust the first proxy hop so req.ip resolves to the real client IP
79
+ // (not the load balancer's IP) when running behind ALB or similar.
80
+ this.app.set('trust proxy', 1)
69
81
  /* Opinionated Express configs */
70
82
  this.app.use(
71
83
  express.json({
@@ -91,7 +103,17 @@ export default class Proxy {
91
103
  // Apply global rate limiting if configured
92
104
  if (this.config.rateLimit && this.config.rateLimit.enabled !== false) {
93
105
  this.logger.info('[Proxy] - [RATE-LIMIT] - Global rate limiting enabled')
94
- const rateLimitMiddleware = this.createRateLimitMiddleware(this.config.rateLimit)
106
+ // Augment the skip function to bypass the global limit for routes that
107
+ // have their own rateLimit config (false = disable, or a RateLimitConfig).
108
+ const globalConfig: GlobalRateLimitConfig = {
109
+ ...this.config.rateLimit,
110
+ skip: (req: express.Request) => {
111
+ const route = this.routeResolver.resolveRoute(req.method as HttpMethod, req.path)
112
+ if (route && route.rateLimit !== undefined) return true
113
+ return this.config.rateLimit!.skip?.(req) ?? false
114
+ },
115
+ }
116
+ const rateLimitMiddleware = this.createRateLimitMiddleware(globalConfig)
95
117
  this.app.use(rateLimitMiddleware)
96
118
  }
97
119
 
@@ -186,12 +208,80 @@ export default class Proxy {
186
208
  this.app
187
209
  .route(this.config.healthCheckRoute || Globals.Listener_HTTP_DefaultHealthCheckRoute)
188
210
  .get(HealthHandler)
211
+ // Register individual routes that declare their own rateLimit config.
212
+ // These are installed BEFORE the wildcard so Express matches them first,
213
+ // giving each route an independent rate limit bucket.
214
+ for (const route of this.config.routes) {
215
+ if (!route.rateLimit) continue
216
+ const rlMiddleware = this.createRouteRateLimitMiddleware(route.rateLimit)
217
+ const paths = Array.isArray(route.path) ? route.path : [route.path]
218
+ for (const path of paths) {
219
+ ;(this.app.route(path) as any)[route.method.toLowerCase()](
220
+ rlMiddleware,
221
+ GenericHandler(this.serverlessHandler)
222
+ )
223
+ }
224
+ }
189
225
  //Main route -- We use a wildcard route because is not the job of the runtime and neither
190
226
  //the task to deny/constrain routes that invoked this task; all the job is done by the
191
227
  //load balancer and we just foward everything we have to the function.
192
228
  this.app.route(Globals.Listener_HTTP_ProxyRoute).all(GenericHandler(this.serverlessHandler))
193
229
  }
194
230
 
231
+ /**
232
+ * Creates rate limiting middleware from a per-route {@link RateLimitConfig},
233
+ * inheriting the global Redis store configuration when available so all
234
+ * rate-limit counters share the same Redis connection.
235
+ * @param {RateLimitConfig} config - The per-route rate limit configuration
236
+ * @returns {express.RequestHandler} Express middleware for rate limiting
237
+ * @private
238
+ */
239
+ private createRouteRateLimitMiddleware(config: RateLimitConfig): express.RequestHandler {
240
+ // Resolve keyGenerator string shorthands to concrete functions.
241
+ let keyGenerator: GlobalRateLimitConfig['keyGenerator']
242
+ if (config.keyGenerator === 'ip') {
243
+ keyGenerator = (req: express.Request) => req.ip || req.socket.remoteAddress || 'unknown'
244
+ } else if (config.keyGenerator === 'userId') {
245
+ keyGenerator = (req: express.Request) => {
246
+ const authHeader = req.headers['authorization']
247
+ if (authHeader?.startsWith('Bearer ')) {
248
+ const parts = authHeader.slice(7).split('.')
249
+ if (parts.length === 3) {
250
+ try {
251
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'))
252
+ const userId = payload.sub || payload.id || payload.userId
253
+ if (userId) return String(userId)
254
+ } catch {
255
+ // malformed token — fall through to IP
256
+ }
257
+ }
258
+ }
259
+ return req.ip || req.socket.remoteAddress || 'unknown'
260
+ }
261
+ } else {
262
+ keyGenerator = config.keyGenerator
263
+ }
264
+
265
+ // Inherit the global Redis store (if configured) so per-route limiters
266
+ // share the same connection; distinguish them with a unique key prefix.
267
+ const globalRl = this.config.rateLimit
268
+ const globalConfig: GlobalRateLimitConfig = {
269
+ windowMs: config.windowMs,
270
+ limit: config.limit,
271
+ keyGenerator,
272
+ skip: config.skip,
273
+ store: globalRl?.store,
274
+ redis: globalRl?.redis
275
+ ? {
276
+ client: globalRl.redis.client,
277
+ prefix: `${globalRl.redis.prefix || 'wapi:rl:'}route:`,
278
+ }
279
+ : undefined,
280
+ }
281
+
282
+ return this.createRateLimitMiddleware(globalConfig)
283
+ }
284
+
195
285
  /**
196
286
  * Creates rate limiting middleware based on the provided configuration.
197
287
  * @param {GlobalRateLimitConfig} config - The rate limit configuration
@@ -211,13 +301,25 @@ export default class Proxy {
211
301
  keyGenerator:
212
302
  config.keyGenerator ||
213
303
  ((req: express.Request) => {
214
- // Use IP address from proxy-aware sources
215
- return (
216
- (req.ip as string) ||
217
- (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
218
- req.socket.remoteAddress ||
219
- 'unknown'
220
- )
304
+ // For authenticated requests, use the stable user ID from the JWT
305
+ // payload so that multiple users behind the same IP are bucketed
306
+ // independently and token rotation doesn't change a user's bucket.
307
+ const authHeader = req.headers['authorization']
308
+ if (authHeader?.startsWith('Bearer ')) {
309
+ const parts = authHeader.slice(7).split('.')
310
+ if (parts.length === 3) {
311
+ try {
312
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'))
313
+ const userId = payload.sub || payload.id || payload.userId
314
+ if (userId) return String(userId)
315
+ } catch {
316
+ // malformed token — fall through to IP
317
+ }
318
+ }
319
+ }
320
+ // Unauthenticated: fall back to real client IP.
321
+ // trust proxy is set so req.ip reflects x-forwarded-for correctly.
322
+ return req.ip || req.socket.remoteAddress || 'unknown'
221
323
  }),
222
324
 
223
325
  // Custom handler when rate limit is exceeded
@@ -3,9 +3,10 @@ import { APIGatewayProxyEvent, Context } from 'aws-lambda'
3
3
  import { expect as c_expect } from 'chai'
4
4
  import request from 'supertest'
5
5
 
6
+ import { HttpMethod } from '../../../../src/API/Request.js'
6
7
  import Globals from '../../../../src/Globals.js'
7
8
  import Proxy from '../../../../src/Server/lib/container/Proxy.js'
8
- import { GlobalRateLimitConfig } from '../../../../src/Server/Router.js'
9
+ import { GlobalRateLimitConfig, RateLimitConfig } from '../../../../src/Server/Router.js'
9
10
  import { defaultUrl } from '../../../Test.utils.js'
10
11
 
11
12
  describe('Rate Limiting', () => {
@@ -548,24 +549,19 @@ describe('Rate Limiting', () => {
548
549
 
549
550
  await proxy.load()
550
551
 
551
- // Test with x-forwarded-for header (covers line 197-198)
552
- const res1 = await request(url)
553
- .get('/test')
554
- .set('x-forwarded-for', '10.0.0.1, 10.0.0.2')
555
- .expect(200)
552
+ // With trust proxy=1, req.ip is the rightmost entry of x-forwarded-for.
553
+ // Two requests with the same rightmost IP share a bucket.
554
+ const res1 = await request(url).get('/test').set('x-forwarded-for', '10.0.0.1').expect(200)
556
555
  c_expect(res1.body).to.deep.equal({ success: true })
557
556
 
558
- // Second request from same IP should be rate limited
559
- const res2 = await request(url)
560
- .get('/test')
561
- .set('x-forwarded-for', '10.0.0.1, 10.0.0.3')
562
- .expect(429)
557
+ // Second request from same IP (same rightmost x-forwarded-for) should be rate limited
558
+ const res2 = await request(url).get('/test').set('x-forwarded-for', '10.0.0.1').expect(429)
563
559
  c_expect(res2.body).to.have.property('error', 'rate_limit_exceeded')
564
560
 
565
561
  // Wait for window to reset
566
562
  await new Promise(resolve => setTimeout(resolve, 1100))
567
563
 
568
- // Different IP should work (covers the split and trim logic)
564
+ // Different IP should have its own clean bucket
569
565
  const res3 = await request(url).get('/test').set('x-forwarded-for', '10.0.0.99').expect(200)
570
566
  c_expect(res3.body).to.deep.equal({ success: true })
571
567
  }, 10000)
@@ -701,6 +697,277 @@ describe('Rate Limiting', () => {
701
697
  // Verify proxy is running
702
698
  c_expect(proxy).to.exist
703
699
  })
700
+
701
+ test('Rate limit default keyGenerator - same bearer token shares a bucket', async () => {
702
+ const port = 57017
703
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
704
+
705
+ // Build a fake but structurally-valid JWT for user1
706
+ const makeJwt = (sub: string) => {
707
+ const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url')
708
+ const payload = Buffer.from(JSON.stringify({ sub })).toString('base64url')
709
+ return `${header}.${payload}.fakesignature`
710
+ }
711
+
712
+ const rateLimitConfig: GlobalRateLimitConfig = {
713
+ enabled: true,
714
+ windowMs: 1000,
715
+ limit: 2,
716
+ // NO custom keyGenerator - should default to bearer token extraction
717
+ }
718
+
719
+ proxy = new Proxy(
720
+ {
721
+ routes: [],
722
+ port,
723
+ rateLimit: rateLimitConfig,
724
+ },
725
+ async (event: APIGatewayProxyEvent, context: Context) => {
726
+ context.succeed({
727
+ body: JSON.stringify({ success: true }),
728
+ statusCode: 200,
729
+ })
730
+ }
731
+ )
732
+
733
+ await proxy.load()
734
+
735
+ const token = makeJwt('user1')
736
+
737
+ // First 2 requests with same token should succeed
738
+ await request(url).get('/test').set('Authorization', `Bearer ${token}`).expect(200)
739
+ await request(url).get('/test').set('Authorization', `Bearer ${token}`).expect(200)
740
+
741
+ // 3rd request from same user (same token sub) should be rate limited
742
+ const rateLimitedRes = await request(url)
743
+ .get('/test')
744
+ .set('Authorization', `Bearer ${token}`)
745
+ .expect(429)
746
+ c_expect(rateLimitedRes.body).to.have.property('error', 'rate_limit_exceeded')
747
+ }, 10000)
748
+
749
+ test('Rate limit default keyGenerator - different users have separate buckets', async () => {
750
+ const port = 57018
751
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
752
+
753
+ const makeJwt = (sub: string) => {
754
+ const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url')
755
+ const payload = Buffer.from(JSON.stringify({ sub })).toString('base64url')
756
+ return `${header}.${payload}.fakesignature`
757
+ }
758
+
759
+ const rateLimitConfig: GlobalRateLimitConfig = {
760
+ enabled: true,
761
+ windowMs: 1000,
762
+ limit: 2,
763
+ }
764
+
765
+ proxy = new Proxy(
766
+ {
767
+ routes: [],
768
+ port,
769
+ rateLimit: rateLimitConfig,
770
+ },
771
+ async (event: APIGatewayProxyEvent, context: Context) => {
772
+ context.succeed({
773
+ body: JSON.stringify({ success: true }),
774
+ statusCode: 200,
775
+ })
776
+ }
777
+ )
778
+
779
+ await proxy.load()
780
+
781
+ const user1Token = makeJwt('user1')
782
+ const user2Token = makeJwt('user2')
783
+
784
+ // User 1 exhausts their bucket
785
+ await request(url).get('/test').set('Authorization', `Bearer ${user1Token}`).expect(200)
786
+ await request(url).get('/test').set('Authorization', `Bearer ${user1Token}`).expect(200)
787
+ await request(url).get('/test').set('Authorization', `Bearer ${user1Token}`).expect(429)
788
+
789
+ // User 2 has their own independent bucket — should still be allowed
790
+ const user2Res = await request(url)
791
+ .get('/test')
792
+ .set('Authorization', `Bearer ${user2Token}`)
793
+ .expect(200)
794
+ c_expect(user2Res.body).to.deep.equal({ success: true })
795
+ }, 10000)
796
+
797
+ test('Rate limit default keyGenerator - unauthenticated requests fall back to IP', async () => {
798
+ const port = 57019
799
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
800
+
801
+ const rateLimitConfig: GlobalRateLimitConfig = {
802
+ enabled: true,
803
+ windowMs: 1000,
804
+ limit: 2,
805
+ }
806
+
807
+ proxy = new Proxy(
808
+ {
809
+ routes: [],
810
+ port,
811
+ rateLimit: rateLimitConfig,
812
+ },
813
+ async (event: APIGatewayProxyEvent, context: Context) => {
814
+ context.succeed({
815
+ body: JSON.stringify({ success: true }),
816
+ statusCode: 200,
817
+ })
818
+ }
819
+ )
820
+
821
+ await proxy.load()
822
+
823
+ // Requests with no Authorization header are bucketed by IP (all from ::1 in tests)
824
+ await request(url).get('/test').expect(200)
825
+ await request(url).get('/test').expect(200)
826
+
827
+ // Same IP, 3rd request exceeds the limit
828
+ const rateLimitedRes = await request(url).get('/test').expect(429)
829
+ c_expect(rateLimitedRes.body).to.have.property('error', 'rate_limit_exceeded')
830
+ }, 10000)
831
+ })
832
+
833
+ describe('Per-route rate limiting', () => {
834
+ // @ts-ignore
835
+ let mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
836
+ let proxy: Proxy | null = null
837
+
838
+ beforeAll(() => {
839
+ // @ts-ignore
840
+ mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
841
+ })
842
+
843
+ afterAll(() => {
844
+ mockExit.mockRestore()
845
+ })
846
+
847
+ beforeEach(() => {
848
+ mockExit.mockReset()
849
+ })
850
+
851
+ afterEach(async () => {
852
+ if (proxy) {
853
+ await proxy.unload()
854
+ proxy = null
855
+ }
856
+ await new Promise(resolve => setTimeout(resolve, 100))
857
+ })
858
+
859
+ test('Per-route rate limit is applied independently of global', async () => {
860
+ const port = 57020
861
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
862
+
863
+ const routeRateLimit: RateLimitConfig = { limit: 2, windowMs: 1000 }
864
+
865
+ proxy = new Proxy(
866
+ {
867
+ routes: [
868
+ {
869
+ path: '/api/limited',
870
+ method: HttpMethod.GET,
871
+ rateLimit: routeRateLimit,
872
+ handler: async () => ({}) as any,
873
+ },
874
+ ],
875
+ port,
876
+ // No global rate limit — only per-route
877
+ },
878
+ async (event: APIGatewayProxyEvent, context: Context) => {
879
+ context.succeed({ body: JSON.stringify({ success: true }), statusCode: 200 })
880
+ }
881
+ )
882
+
883
+ await proxy.load()
884
+
885
+ // First 2 requests succeed
886
+ await request(url).get('/api/limited').expect(200)
887
+ await request(url).get('/api/limited').expect(200)
888
+
889
+ // 3rd request is blocked by the per-route rate limit
890
+ const res = await request(url).get('/api/limited').expect(429)
891
+ c_expect(res.body).to.have.property('error', 'rate_limit_exceeded')
892
+
893
+ // Wait for the window to reset and verify recovery
894
+ await new Promise(resolve => setTimeout(resolve, 1100))
895
+ await request(url).get('/api/limited').expect(200)
896
+ }, 10000)
897
+
898
+ test('rateLimit: false exempts a route from the global rate limit', async () => {
899
+ const port = 57021
900
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
901
+
902
+ proxy = new Proxy(
903
+ {
904
+ routes: [
905
+ {
906
+ path: '/api/exempt',
907
+ method: HttpMethod.GET,
908
+ rateLimit: false,
909
+ handler: async () => ({}) as any,
910
+ },
911
+ ],
912
+ port,
913
+ rateLimit: { enabled: true, limit: 2, windowMs: 5000 },
914
+ },
915
+ async (event: APIGatewayProxyEvent, context: Context) => {
916
+ context.succeed({ body: JSON.stringify({ success: true }), statusCode: 200 })
917
+ }
918
+ )
919
+
920
+ await proxy.load()
921
+
922
+ // Global limit is 2, but /api/exempt bypasses it — all 5 requests should succeed
923
+ for (let i = 0; i < 5; i++) {
924
+ const res = await request(url).get('/api/exempt').expect(200)
925
+ c_expect(res.body).to.deep.equal({ success: true })
926
+ }
927
+
928
+ // A non-exempt path still hits the global limit
929
+ await request(url).get('/other-path').expect(200)
930
+ await request(url).get('/other-path').expect(200)
931
+ await request(url).get('/other-path').expect(429)
932
+ }, 10000)
933
+
934
+ test('Per-route and global limits operate independently', async () => {
935
+ const port = 57022
936
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
937
+
938
+ proxy = new Proxy(
939
+ {
940
+ routes: [
941
+ {
942
+ path: '/api/strict',
943
+ method: HttpMethod.GET,
944
+ rateLimit: { limit: 2, windowMs: 5000 },
945
+ handler: async () => ({}) as any,
946
+ },
947
+ ],
948
+ port,
949
+ rateLimit: { enabled: true, limit: 5, windowMs: 5000 },
950
+ },
951
+ async (event: APIGatewayProxyEvent, context: Context) => {
952
+ context.succeed({ body: JSON.stringify({ success: true }), statusCode: 200 })
953
+ }
954
+ )
955
+
956
+ await proxy.load()
957
+
958
+ // /api/strict has a tighter per-route limit (2); hits 429 on 3rd request
959
+ await request(url).get('/api/strict').expect(200)
960
+ await request(url).get('/api/strict').expect(200)
961
+ await request(url).get('/api/strict').expect(429)
962
+
963
+ // /api/strict requests do NOT consume global tokens — /other-path still has its full budget
964
+ for (let i = 0; i < 5; i++) {
965
+ const res = await request(url).get('/other-path').expect(200)
966
+ c_expect(res.body).to.deep.equal({ success: true })
967
+ }
968
+ // 6th request to /other-path exhausts the global limit
969
+ await request(url).get('/other-path').expect(429)
970
+ }, 15000)
704
971
  })
705
972
 
706
973
  export {}