@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.
- package/dist/package-lock.json +104 -65
- package/dist/package.json +3 -3
- package/dist/src/Server/lib/container/Proxy.d.ts +15 -0
- package/dist/src/Server/lib/container/Proxy.js +104 -7
- package/dist/src/Server/lib/container/Proxy.js.map +1 -1
- package/package.json +3 -3
- package/src/Server/lib/container/Proxy.ts +111 -9
- package/tests/Server/lib/container/RateLimit.test.ts +279 -12
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@creator.co/wapi",
|
|
3
|
-
"version": "1.
|
|
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
|
+
"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.
|
|
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.
|
|
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.
|
|
6041
|
-
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.
|
|
6042
|
-
"integrity": "sha512-
|
|
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.
|
|
6050
|
-
"iconv-lite": "0.4.24",
|
|
6051
|
-
"on-finished": "2.4.1",
|
|
6052
|
-
"qs": "6.
|
|
6053
|
-
"raw-body": "2.5.
|
|
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.
|
|
8314
|
-
"resolved": "https://registry.npmjs.org/express/-/express-4.22.
|
|
8315
|
-
"integrity": "sha512-
|
|
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.
|
|
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.
|
|
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.
|
|
11184
|
-
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.
|
|
11185
|
-
"integrity": "sha512-
|
|
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.
|
|
12872
|
-
"resolved": "https://registry.npmjs.org/qs/-/qs-6.
|
|
12873
|
-
"integrity": "sha512
|
|
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
|
|
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.
|
|
12936
|
-
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.
|
|
12937
|
-
"integrity": "sha512-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
'
|
|
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":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,EAAwB,YAAY,EAAE,MAAM,MAAM,CAAA;AAEzD,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAG7C,OAAO,cAAc,MAAM,qBAAqB,CAAA;AAChD,OAAO,aAAa,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"Proxy.js","sourceRoot":"","sources":["../../../../../src/Server/lib/container/Proxy.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,EAAwB,YAAY,EAAE,MAAM,MAAM,CAAA;AAEzD,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAG7C,OAAO,cAAc,MAAM,qBAAqB,CAAA;AAChD,OAAO,aAAa,MAAM,oBAAoB,CAAA;AAE9C,OAAO,OAAO,MAAM,qBAAqB,CAAA;AACzC,OAAO,MAAM,MAAM,2BAA2B,CAAA;AAC9C,OAAO,KAAK,MAAM,wBAAwB,CAAA;AAE1C,OAAO,aAAa,MAAM,wBAAwB,CAAA;AAElD,+CAA+C;AAC/C,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAA;AAEtF;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,KAAK;IAwCxB;;;;;OAKG;IACH,YAAY,MAAoB,EAAE,iBAAkD;QAClF,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAA;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAA;QAC1C,IAAI,CAAC,aAAa,GAAG,IAAI,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACnD,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAA;QACjE,IAAI,CAAC,GAAG,GAAG,OAAO,EAAE,CAAA;QACpB,qEAAqE;QACrE,mEAAmE;QACnE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAA;QAC9B,iCAAiC;QACjC,IAAI,CAAC,GAAG,CAAC,GAAG,CACV,OAAO,CAAC,IAAI,CAAC;YACX,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG;gBAClB,GAAG,CAAC,SAAS,CAAC,GAAG,GAAG,CAAA;YACtB,CAAC;SACF,CAAC,CACH,CAAA;QACD,oBAAoB;QACpB,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,sBAAsB,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACrF,IAAI,CAAC,GAAG,CAAC,GAAG,CACV,IAAI,CACF,UAAU;YACR,CAAC,CAAC;gBACE,MAAM,EAAE,UAAU,CAAC,MAAM;gBACzB,cAAc,EAAE,UAAU,CAAC,OAAO;gBAClC,WAAW,EAAE,CAAC,CAAC,UAAU,CAAC,gBAAgB;aAC3C;YACH,CAAC,CAAC,EAAE,CACP,CACF,CAAA;QAED,2CAA2C;QAC3C,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YACrE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,uDAAuD,CAAC,CAAA;YACzE,uEAAuE;YACvE,2EAA2E;YAC3E,MAAM,YAAY,mCACb,IAAI,CAAC,MAAM,CAAC,SAAS,KACxB,IAAI,EAAE,CAAC,GAAoB,EAAE,EAAE;;oBAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,GAAG,CAAC,MAAoB,EAAE,GAAG,CAAC,IAAI,CAAC,CAAA;oBACjF,IAAI,KAAK,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;wBAAE,OAAO,IAAI,CAAA;oBACvD,OAAO,MAAA,MAAA,MAAA,IAAI,CAAC,MAAM,CAAC,SAAU,EAAC,IAAI,mDAAG,GAAG,CAAC,mCAAI,KAAK,CAAA;gBACpD,CAAC,GACF,CAAA;YACD,MAAM,mBAAmB,GAAG,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAA;YACxE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;QACnC,CAAC;QAED,6EAA6E;QAC7E,iFAAiF;QACjF,gFAAgF;QAChF,mFAAmF;QACnF,mBAAmB;QACnB,kDAAkD;QAClD,gDAAgD;IAClD,CAAC;IAED;;;OAGG;IACU,IAAI;;YACf,MAAM,IAAI,CAAC,cAAc,EAAE,CAAA;YAC3B,IAAI,CAAC,aAAa,EAAE,CAAA;QACtB,CAAC;KAAA;IAED;;;;OAIG;IACU,MAAM,CAAC,GAAS;;YAC3B,MAAM,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;QAC/B,CAAC;KAAA;IAED;;;OAGG;IACW,cAAc;;YAC1B,qDAAqD;YACrD,OAAO,IAAI,OAAO,CAAC,CAAM,OAAO,EAAC,EAAE;gBACjC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,yBAAyB,CAAA;gBAClE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,4BAA4B,UAAU,OAAO,IAAI,EAAE,CAAC,CAAA;gBACrE,gBAAgB;gBAChB,IAAI,CAAC,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBACtC,eAAe;gBACf,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,CAAC,4BAA4B,CAAC,CAAA;gBACrF,0EAA0E;gBAC1E,8BAA8B;gBAC9B,IAAI,CAAC,QAAQ,CAAC,gBAAgB,GAAG,KAAK,CAAA;gBACtC,IAAI,CAAC,QAAQ,CAAC,cAAc,GAAG,KAAK,CAAA;gBAEpC,yBAAyB;gBACzB,IAAI,IAAI,CAAC,MAAM,CAAC,kBAAkB;oBAChC,MAAM,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;gBAC/D,eAAe;gBACf,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;oBAC9B,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAA;oBAClC,OAAO,EAAE,CAAA;gBACX,CAAC,CAAC,CAAA;YACJ,CAAC,CAAA,CAAC,CAAA;QACJ,CAAC;KAAA;IAED;;;;OAIG;IACW,aAAa,CAAC,GAAS;;YACnC,IAAI,IAAI,CAAC,QAAQ;gBAAE,OAAM;YACzB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;YACpB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAA;YACxC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;gBAC3B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;oBACzB,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,CAAA;oBACxB,IAAI,IAAI;wBAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAA;oBAC3D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAA;oBACvC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;oBAC1B,OAAO,CAAC,IAAI,CAAC,CAAA;gBACf,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;QACJ,CAAC;KAAA;IAED;;;OAGG;IACK,aAAa;QACnB,+DAA+D;QAC/D,mDAAmD;QACnD,OAAO,CAAC,GAAG,CACT,8BACE,IAAI,CAAC,MAAM,CAAC,gBAAgB,IAAI,OAAO,CAAC,qCAC1C,EAAE,CACH,CAAA;QACD,IAAI,CAAC,GAAG;aACL,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,gBAAgB,IAAI,OAAO,CAAC,qCAAqC,CAAC;aACpF,GAAG,CAAC,aAAa,CAAC,CAAA;QACrB,sEAAsE;QACtE,yEAAyE;QACzE,sDAAsD;QACtD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACvC,IAAI,CAAC,KAAK,CAAC,SAAS;gBAAE,SAAQ;YAC9B,MAAM,YAAY,GAAG,IAAI,CAAC,8BAA8B,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;YACzE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACnE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,CAAC;gBAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAS,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CACxD,YAAY,EACZ,cAAc,CAAC,IAAI,CAAC,iBAAiB,CAAC,CACvC,CAAA;YACH,CAAC;QACH,CAAC;QACD,yFAAyF;QACzF,sFAAsF;QACtF,sEAAsE;QACtE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAA;IAC9F,CAAC;IAED;;;;;;;OAOG;IACK,8BAA8B,CAAC,MAAuB;QAC5D,gEAAgE;QAChE,IAAI,YAAmD,CAAA;QACvD,IAAI,MAAM,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;YACjC,YAAY,GAAG,CAAC,GAAoB,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,SAAS,CAAA;QAC1F,CAAC;aAAM,IAAI,MAAM,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;YAC5C,YAAY,GAAG,CAAC,GAAoB,EAAE,EAAE;gBACtC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAA;gBAC/C,IAAI,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;oBACtC,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;oBAC5C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBACvB,IAAI,CAAC;4BACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;4BAC/E,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE,IAAI,OAAO,CAAC,MAAM,CAAA;4BAC1D,IAAI,MAAM;gCAAE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAA;wBACnC,CAAC;wBAAC,WAAM,CAAC;4BACP,uCAAuC;wBACzC,CAAC;oBACH,CAAC;gBACH,CAAC;gBACD,OAAO,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,SAAS,CAAA;YACxD,CAAC,CAAA;QACH,CAAC;aAAM,CAAC;YACN,YAAY,GAAG,MAAM,CAAC,YAAY,CAAA;QACpC,CAAC;QAED,uEAAuE;QACvE,wEAAwE;QACxE,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAA;QACtC,MAAM,YAAY,GAA0B;YAC1C,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,YAAY;YACZ,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,KAAK,EAAE,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,KAAK;YACtB,KAAK,EAAE,CAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,KAAK;gBACpB,CAAC,CAAC;oBACE,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM;oBAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,IAAI,UAAU,QAAQ;iBACvD;gBACH,CAAC,CAAC,SAAS;SACd,CAAA;QAED,OAAO,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAA;IACrD,CAAC;IAED;;;;;OAKG;IACK,yBAAyB,CAAC,MAA6B;QAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAA;QAE/C,OAAO,SAAS,CAAC;YACf,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,KAAK,EAAE,oBAAoB;YACxD,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,oCAAoC;YAC/D,eAAe,EAAE,IAAI,EAAE,kDAAkD;YACzE,aAAa,EAAE,KAAK,EAAE,kCAAkC;YAExD,iDAAiD;YACjD,YAAY,EACV,MAAM,CAAC,YAAY;gBACnB,CAAC,CAAC,GAAoB,EAAE,EAAE;oBACxB,kEAAkE;oBAClE,iEAAiE;oBACjE,mEAAmE;oBACnE,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAA;oBAC/C,IAAI,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;wBACtC,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;wBAC5C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;4BACvB,IAAI,CAAC;gCACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;gCAC/E,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE,IAAI,OAAO,CAAC,MAAM,CAAA;gCAC1D,IAAI,MAAM;oCAAE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAA;4BACnC,CAAC;4BAAC,WAAM,CAAC;gCACP,uCAAuC;4BACzC,CAAC;wBACH,CAAC;oBACH,CAAC;oBACD,gDAAgD;oBAChD,mEAAmE;oBACnE,OAAO,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,SAAS,CAAA;gBACxD,CAAC,CAAC;YAEJ,6CAA6C;YAC7C,OAAO,EACL,MAAM,CAAC,OAAO;gBACd,CAAC,CAAC,GAAoB,EAAE,GAAqB,EAAE,EAAE;oBAC/C,2BAA2B;oBAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yCAAyC,EAAE;wBAC1D,EAAE,EAAE,GAAG,CAAC,EAAE;wBACV,IAAI,EAAE,GAAG,CAAC,IAAI;wBACd,MAAM,EAAE,GAAG,CAAC,MAAM;wBAClB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;qBACpC,CAAC,CAAA;oBAEF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;wBACnB,KAAK,EAAE,qBAAqB;wBAC5B,OAAO,EAAE,4CAA4C;qBACtD,CAAC,CAAA;gBACJ,CAAC,CAAC;YAEJ,sEAAsE;YACtE,IAAI,EAAE,MAAM,CAAC,IAAI;YAEjB,uDAAuD;YACvD,KAAK,EAAE,KAAK;SACb,CAAC,CAAA;IACJ,CAAC;IAED;;;;;OAKG;IACK,oBAAoB,CAAC,MAA6B;;QACxD,IAAI,MAAM,CAAC,KAAK,KAAK,OAAO,KAAI,MAAA,MAAM,CAAC,KAAK,0CAAE,MAAM,CAAA,EAAE,CAAC;YACrD,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAA;YACzD,OAAO,IAAI,UAAU,CAAC;gBACpB,WAAW,EAAE,CAAC,GAAG,IAAc,EAAE,EAAE,CAAC,MAAM,CAAC,KAAM,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC;gBAC1E,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,UAAU;aAC1C,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAA;QAC7D,OAAO,SAAS,CAAA,CAAC,iDAAiD;IACpE,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@creator.co/wapi",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
'
|
|
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
|
-
//
|
|
552
|
-
|
|
553
|
-
|
|
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
|
|
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 {}
|