@mpen/routekit 0.1.0 → 0.1.2
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/bin.d.mts +4 -0
- package/dist/client/react.d.mts +178 -0
- package/dist/client/react.mjs +142 -0
- package/dist/client.d.mts +433 -0
- package/dist/client.mjs +264 -0
- package/dist/content-BuDOmhH_.mjs +102 -0
- package/dist/core-CzUCxvGk.d.mts +140 -0
- package/dist/core-DbmQauwS.mjs +81 -0
- package/dist/handlers.d.mts +72 -0
- package/dist/handlers.mjs +153 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1152 -0
- package/dist/middleware.d.mts +388 -0
- package/dist/middleware.mjs +1222 -0
- package/dist/request-Dn0zc-xm.mjs +1025 -0
- package/dist/response/content.d.mts +79 -0
- package/dist/response/content.mjs +2 -0
- package/dist/response/json-rpc.d.mts +1 -0
- package/dist/response/json-rpc.mjs +1 -0
- package/dist/response/problem/valibot.d.mts +230 -0
- package/dist/response/problem/valibot.mjs +258 -0
- package/dist/response/problem.d.mts +415 -0
- package/dist/response/problem.mjs +183 -0
- package/dist/response/status.d.mts +45 -0
- package/dist/response/status.mjs +2 -0
- package/dist/responses-B379Ep9Y.d.mts +296 -0
- package/dist/responses-BpVrgeYi.mjs +101 -0
- package/dist/router-Cwb7ak0J.d.mts +1819 -0
- package/dist/routes.d.mts +282 -0
- package/dist/routes.mjs +311 -0
- package/dist/status-C-8mw-FB.mjs +59 -0
- package/dist/valibot-D7liFYyB.d.mts +290 -0
- package/dist/valibot-Du97X-TS.mjs +326 -0
- package/package.json +8 -2
- package/src/bin/gen-api-client.test.ts +0 -70
- package/src/bin/gen-api-client.ts +0 -986
- package/src/client/headers.ts +0 -31
- package/src/client/index.ts +0 -8
- package/src/client/promise.ts +0 -11
- package/src/client/react/index.test.tsx +0 -266
- package/src/client/react/index.ts +0 -431
- package/src/client/responses.test.ts +0 -151
- package/src/client/responses.ts +0 -278
- package/src/client/transport.ts +0 -74
- package/src/client/transports/body-codec.ts +0 -61
- package/src/client/transports/fetch.ts +0 -113
- package/src/client/tsconfig.json +0 -9
- package/src/client/types.ts +0 -15
- package/src/client/url.ts +0 -31
- package/src/index.ts +0 -63
- package/src/router/fetch-types.ts +0 -13
- package/src/router/handlers/index.ts +0 -2
- package/src/router/handlers/openapi/index.ts +0 -2
- package/src/router/handlers/openapi/openapi.ts +0 -293
- package/src/router/integration/zod-openapi.test.ts +0 -74
- package/src/router/lib/charset.test.ts +0 -22
- package/src/router/lib/charset.ts +0 -133
- package/src/router/lib/collections.ts +0 -3
- package/src/router/lib/format.test.ts +0 -67
- package/src/router/lib/format.ts +0 -35
- package/src/router/lib/host.ts +0 -4
- package/src/router/lib/json-schema.ts +0 -6
- package/src/router/lib/media-type.test.ts +0 -122
- package/src/router/lib/media-type.ts +0 -289
- package/src/router/lib/pathname.test.ts +0 -18
- package/src/router/lib/pathname.ts +0 -19
- package/src/router/lib/route-names.ts +0 -70
- package/src/router/lib/route-normalize.test.ts +0 -36
- package/src/router/lib/route-normalize.ts +0 -67
- package/src/router/lib/schema-merge.ts +0 -56
- package/src/router/middleware/accept-ctx.test.ts +0 -33
- package/src/router/middleware/accept-ctx.ts +0 -12
- package/src/router/middleware/body-limit.test.ts +0 -112
- package/src/router/middleware/body-limit.ts +0 -121
- package/src/router/middleware/content-type-context.ts +0 -0
- package/src/router/middleware/cors.test.ts +0 -269
- package/src/router/middleware/cors.ts +0 -490
- package/src/router/middleware/csrf.test.ts +0 -106
- package/src/router/middleware/csrf.ts +0 -192
- package/src/router/middleware/define.ts +0 -249
- package/src/router/middleware/index.ts +0 -34
- package/src/router/middleware/jsxhtml-response.ts +0 -0
- package/src/router/middleware/oas-swagger.ts +0 -0
- package/src/router/middleware/rate-limit.test.ts +0 -886
- package/src/router/middleware/rate-limit.ts +0 -920
- package/src/router/middleware/request-id-ctx.test.ts +0 -183
- package/src/router/middleware/request-id-ctx.ts +0 -135
- package/src/router/middleware/request-logger-format.test.ts +0 -16
- package/src/router/middleware/request-logger-format.ts +0 -269
- package/src/router/middleware/request-logger.test.ts +0 -267
- package/src/router/middleware/request-logger.ts +0 -131
- package/src/router/middleware/start-time-ctx.ts +0 -5
- package/src/router/request.ts +0 -611
- package/src/router/response/core.ts +0 -181
- package/src/router/response/directives.ts +0 -233
- package/src/router/response/formats/content/bodyless.ts +0 -54
- package/src/router/response/formats/content/content.ts +0 -79
- package/src/router/response/formats/content/index.ts +0 -2
- package/src/router/response/formats/json-rpc/index.ts +0 -2
- package/src/router/response/formats/problem/badRequest.ts +0 -90
- package/src/router/response/formats/problem/conflict.ts +0 -90
- package/src/router/response/formats/problem/created.ts +0 -40
- package/src/router/response/formats/problem/index.ts +0 -27
- package/src/router/response/formats/problem/notFound.ts +0 -90
- package/src/router/response/formats/problem/permissionDenied.ts +0 -90
- package/src/router/response/formats/problem/problem.test.ts +0 -888
- package/src/router/response/formats/problem/rateLimited.ts +0 -90
- package/src/router/response/formats/problem/responses.ts +0 -219
- package/src/router/response/formats/problem/root-errors.ts +0 -48
- package/src/router/response/formats/problem/sessionExpired.ts +0 -90
- package/src/router/response/formats/problem/types.ts +0 -170
- package/src/router/response/formats/problem/unauthenticated.ts +0 -90
- package/src/router/response/formats/problem/valibot.ts +0 -410
- package/src/router/response/formats/status/index.ts +0 -1
- package/src/router/response/formats/status/responses.ts +0 -59
- package/src/router/response/formats/status/status.test.ts +0 -21
- package/src/router/response/framers.ts +0 -85
- package/src/router/response/index.ts +0 -28
- package/src/router/response/openapi.test.ts +0 -96
- package/src/router/response/openapi.ts +0 -1
- package/src/router/response/serializers.ts +0 -66
- package/src/router/response/stream.ts +0 -35
- package/src/router/router.test.ts +0 -1571
- package/src/router/router.ts +0 -1965
- package/src/router/routes/index.ts +0 -46
- package/src/router/routes/valibot/index.ts +0 -18
- package/src/router/routes/valibot/valibot.ts +0 -1393
- package/src/router/routes/valibot.test.ts +0 -286
- package/src/router/routes/zod/index.ts +0 -18
- package/src/router/routes/zod/zod.ts +0 -1318
- package/src/router/routes/zod.test.ts +0 -280
- package/src/router/server-interface.ts +0 -31
- package/src/router/types.ts +0 -657
|
@@ -1,886 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it } from 'bun:test'
|
|
3
|
-
import { HttpMethod } from '@mpen/http'
|
|
4
|
-
import { existsSync } from 'node:fs'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import { Router } from '../router'
|
|
7
|
-
import { rateLimit } from './rate-limit'
|
|
8
|
-
|
|
9
|
-
const asnDbPath = fileURLToPath(new URL('../testing/GeoLite2-ASN.mmdb', import.meta.url))
|
|
10
|
-
const countryDbPath = fileURLToPath(new URL('../testing/GeoLite2-Country.mmdb', import.meta.url))
|
|
11
|
-
const hasMaxmindDbs = existsSync(asnDbPath) && existsSync(countryDbPath)
|
|
12
|
-
|
|
13
|
-
class CaptureStorage {
|
|
14
|
-
keys: string[] = []
|
|
15
|
-
|
|
16
|
-
async readCounter(_ctx: unknown, _key: string) {
|
|
17
|
-
return null
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async writeCounter(_ctx: unknown, key: string, _counter: unknown, _ttlMs: number) {
|
|
21
|
-
this.keys.push(key)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
26
|
-
|
|
27
|
-
describe(rateLimit.name, () => {
|
|
28
|
-
it('declares its throttled response on affected routes', () => {
|
|
29
|
-
const router = new Router()
|
|
30
|
-
router.use(
|
|
31
|
-
rateLimit({
|
|
32
|
-
getUserId: async () => 'user-1',
|
|
33
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
34
|
-
baseWindowMs: 1000,
|
|
35
|
-
baseMaxRequestsPerBaseWindow: 2,
|
|
36
|
-
anonymousIpMultiplier: 1,
|
|
37
|
-
addRetryAfterHeader: false,
|
|
38
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
39
|
-
endpointLimits: [],
|
|
40
|
-
includeQueryInEndpointKey: false,
|
|
41
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
42
|
-
}),
|
|
43
|
-
)
|
|
44
|
-
router.add({ method: HttpMethod.GET, path: '/', handler: () => new Response() })
|
|
45
|
-
|
|
46
|
-
expect(router.getRoutes()[0]?.schema?.response?.body).toEqual({
|
|
47
|
-
429: { type: 'string' },
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('enforces per-user identity limits', async () => {
|
|
52
|
-
const router = new Router()
|
|
53
|
-
router.use(
|
|
54
|
-
rateLimit({
|
|
55
|
-
getUserId: async () => 'user-1',
|
|
56
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
57
|
-
baseWindowMs: 1000,
|
|
58
|
-
baseMaxRequestsPerBaseWindow: 2,
|
|
59
|
-
anonymousIpMultiplier: 1,
|
|
60
|
-
addRetryAfterHeader: false,
|
|
61
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
62
|
-
endpointLimits: [],
|
|
63
|
-
includeQueryInEndpointKey: false,
|
|
64
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
65
|
-
}),
|
|
66
|
-
)
|
|
67
|
-
router.add({
|
|
68
|
-
method: HttpMethod.GET,
|
|
69
|
-
path: '/',
|
|
70
|
-
handler: () => new Response('ok'),
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const makeRequest = () =>
|
|
74
|
-
new Request('https://example.com/', {
|
|
75
|
-
headers: { 'x-forwarded-for': '203.0.113.5' },
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
const response1 = await router.fetch(makeRequest())
|
|
79
|
-
const response2 = await router.fetch(makeRequest())
|
|
80
|
-
const response3 = await router.fetch(makeRequest())
|
|
81
|
-
|
|
82
|
-
expect(response1.status).toBe(200)
|
|
83
|
-
expect(response2.status).toBe(200)
|
|
84
|
-
expect(response3.status).toBe(429)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('applies endpoint limits to identity and subnet', async () => {
|
|
88
|
-
const router = new Router()
|
|
89
|
-
router.use(
|
|
90
|
-
rateLimit({
|
|
91
|
-
getUserId: async () => null,
|
|
92
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
93
|
-
baseWindowMs: 1000,
|
|
94
|
-
baseMaxRequestsPerBaseWindow: 100,
|
|
95
|
-
anonymousIpMultiplier: 1,
|
|
96
|
-
addRetryAfterHeader: true,
|
|
97
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
98
|
-
endpointLimits: [{ pattern: '/limited', limit: { GET: 1 } }],
|
|
99
|
-
includeQueryInEndpointKey: false,
|
|
100
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
101
|
-
}),
|
|
102
|
-
)
|
|
103
|
-
router.add({
|
|
104
|
-
method: HttpMethod.GET,
|
|
105
|
-
path: '/limited',
|
|
106
|
-
handler: () => new Response('ok'),
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
const makeRequest = () =>
|
|
110
|
-
new Request('https://example.com/limited', {
|
|
111
|
-
headers: { 'x-forwarded-for': '203.0.113.7' },
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
const response1 = await router.fetch(makeRequest())
|
|
115
|
-
const response2 = await router.fetch(makeRequest())
|
|
116
|
-
|
|
117
|
-
expect(response1.status).toBe(200)
|
|
118
|
-
expect(response2.status).toBe(429)
|
|
119
|
-
expect(response2.headers.get('retry-after')).not.toBeNull()
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('avoids duplicate identity prefixes in endpoint keys', async () => {
|
|
123
|
-
const storage = new CaptureStorage()
|
|
124
|
-
const router = new Router()
|
|
125
|
-
router.use(
|
|
126
|
-
rateLimit({
|
|
127
|
-
getUserId: async () => 'user-1',
|
|
128
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
129
|
-
baseWindowMs: 1000,
|
|
130
|
-
baseMaxRequestsPerBaseWindow: 10,
|
|
131
|
-
anonymousIpMultiplier: 1,
|
|
132
|
-
addRetryAfterHeader: false,
|
|
133
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
134
|
-
endpointLimits: [{ pattern: '/endpoint', limit: { GET: 1 } }],
|
|
135
|
-
includeQueryInEndpointKey: false,
|
|
136
|
-
storage,
|
|
137
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
138
|
-
}),
|
|
139
|
-
)
|
|
140
|
-
router.add({
|
|
141
|
-
method: HttpMethod.GET,
|
|
142
|
-
path: '/endpoint',
|
|
143
|
-
handler: () => new Response('ok'),
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
await router.fetch(
|
|
147
|
-
new Request('https://example.com/endpoint', {
|
|
148
|
-
headers: { 'x-forwarded-for': '203.0.113.11' },
|
|
149
|
-
}),
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
expect(storage.keys.some((key) => key.includes('identity:identity:'))).toBe(false)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('normalizes query params by default', async () => {
|
|
156
|
-
const router = new Router()
|
|
157
|
-
router.use(
|
|
158
|
-
rateLimit({
|
|
159
|
-
getUserId: async () => null,
|
|
160
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
161
|
-
baseWindowMs: 1000,
|
|
162
|
-
baseMaxRequestsPerBaseWindow: 10,
|
|
163
|
-
anonymousIpMultiplier: 1,
|
|
164
|
-
addRetryAfterHeader: false,
|
|
165
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
166
|
-
endpointLimits: [{ pattern: '/search', limit: { GET: 1 } }],
|
|
167
|
-
includeQueryInEndpointKey: true,
|
|
168
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
169
|
-
}),
|
|
170
|
-
)
|
|
171
|
-
router.add({
|
|
172
|
-
method: HttpMethod.GET,
|
|
173
|
-
path: '/search',
|
|
174
|
-
handler: () => new Response('ok'),
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
const request1 = new Request('https://example.com/search?a=1&b=2', {
|
|
178
|
-
headers: { 'x-forwarded-for': '203.0.113.70' },
|
|
179
|
-
})
|
|
180
|
-
const request2 = new Request('https://example.com/search?b=2&a=1', {
|
|
181
|
-
headers: { 'x-forwarded-for': '203.0.113.70' },
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
const response1 = await router.fetch(request1)
|
|
185
|
-
const response2 = await router.fetch(request2)
|
|
186
|
-
|
|
187
|
-
expect(response1.status).toBe(200)
|
|
188
|
-
expect(response2.status).toBe(429)
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it('enforces ipv4 subnet limits', async () => {
|
|
192
|
-
const router = new Router()
|
|
193
|
-
router.use(
|
|
194
|
-
rateLimit({
|
|
195
|
-
getUserId: async () => 'user-1',
|
|
196
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
197
|
-
baseWindowMs: 1000,
|
|
198
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
199
|
-
anonymousIpMultiplier: 1,
|
|
200
|
-
addRetryAfterHeader: false,
|
|
201
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
202
|
-
endpointLimits: [],
|
|
203
|
-
includeQueryInEndpointKey: false,
|
|
204
|
-
scales: { subnet: { ipv4: 0.2, ipv6: 10 } },
|
|
205
|
-
}),
|
|
206
|
-
)
|
|
207
|
-
router.add({
|
|
208
|
-
method: HttpMethod.GET,
|
|
209
|
-
path: '/ipv4',
|
|
210
|
-
handler: () => new Response('ok'),
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
const request = () =>
|
|
214
|
-
new Request('https://example.com/ipv4', {
|
|
215
|
-
headers: { 'x-forwarded-for': '203.0.113.10' },
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
const response1 = await router.fetch(request())
|
|
219
|
-
const response2 = await router.fetch(request())
|
|
220
|
-
|
|
221
|
-
expect(response1.status).toBe(200)
|
|
222
|
-
expect(response2.status).toBe(429)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('enforces ipv6 subnet limits', async () => {
|
|
226
|
-
const router = new Router()
|
|
227
|
-
router.use(
|
|
228
|
-
rateLimit({
|
|
229
|
-
getUserId: async () => 'user-1',
|
|
230
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
231
|
-
baseWindowMs: 1000,
|
|
232
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
233
|
-
anonymousIpMultiplier: 1,
|
|
234
|
-
addRetryAfterHeader: false,
|
|
235
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
236
|
-
endpointLimits: [],
|
|
237
|
-
includeQueryInEndpointKey: false,
|
|
238
|
-
scales: { subnet: { ipv4: 10, ipv6: 0.2 } },
|
|
239
|
-
}),
|
|
240
|
-
)
|
|
241
|
-
router.add({
|
|
242
|
-
method: HttpMethod.GET,
|
|
243
|
-
path: '/ipv6',
|
|
244
|
-
handler: () => new Response('ok'),
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
const request = () =>
|
|
248
|
-
new Request('https://example.com/ipv6', {
|
|
249
|
-
headers: { 'x-forwarded-for': '2001:db8:abcd:12::1' },
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
const response1 = await router.fetch(request())
|
|
253
|
-
const response2 = await router.fetch(request())
|
|
254
|
-
|
|
255
|
-
expect(response1.status).toBe(200)
|
|
256
|
-
expect(response2.status).toBe(429)
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('applies a conservative subnet scale for unknown IPs', async () => {
|
|
260
|
-
const router = new Router()
|
|
261
|
-
router.use(
|
|
262
|
-
rateLimit({
|
|
263
|
-
getUserId: async () => 'user-1',
|
|
264
|
-
getIpAddress: async () => 'not-an-ip',
|
|
265
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
266
|
-
baseWindowMs: 1000,
|
|
267
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
268
|
-
anonymousIpMultiplier: 1,
|
|
269
|
-
addRetryAfterHeader: false,
|
|
270
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
271
|
-
endpointLimits: [],
|
|
272
|
-
includeQueryInEndpointKey: false,
|
|
273
|
-
scales: { subnet: { ipv4: 0.4, ipv6: 0.2 } },
|
|
274
|
-
}),
|
|
275
|
-
)
|
|
276
|
-
router.add({
|
|
277
|
-
method: HttpMethod.GET,
|
|
278
|
-
path: '/unknown-ip',
|
|
279
|
-
handler: () => new Response('ok'),
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
const request = () => new Request('https://example.com/unknown-ip')
|
|
283
|
-
|
|
284
|
-
const response1 = await router.fetch(request())
|
|
285
|
-
const response2 = await router.fetch(request())
|
|
286
|
-
|
|
287
|
-
expect(response1.status).toBe(200)
|
|
288
|
-
expect(response2.status).toBe(429)
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
it('uses ipv4-mapped IPv6 addresses as ipv4', async () => {
|
|
292
|
-
const router = new Router()
|
|
293
|
-
router.use(
|
|
294
|
-
rateLimit({
|
|
295
|
-
getUserId: async () => 'user-1',
|
|
296
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
297
|
-
baseWindowMs: 1000,
|
|
298
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
299
|
-
anonymousIpMultiplier: 1,
|
|
300
|
-
addRetryAfterHeader: false,
|
|
301
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
302
|
-
endpointLimits: [],
|
|
303
|
-
includeQueryInEndpointKey: false,
|
|
304
|
-
scales: { subnet: { ipv4: 0.2, ipv6: 10 } },
|
|
305
|
-
}),
|
|
306
|
-
)
|
|
307
|
-
router.add({
|
|
308
|
-
method: HttpMethod.GET,
|
|
309
|
-
path: '/ipv4-mapped',
|
|
310
|
-
handler: () => new Response('ok'),
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
const request = () =>
|
|
314
|
-
new Request('https://example.com/ipv4-mapped', {
|
|
315
|
-
headers: { 'x-forwarded-for': '::ffff:203.0.113.80' },
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
const response1 = await router.fetch(request())
|
|
319
|
-
const response2 = await router.fetch(request())
|
|
320
|
-
|
|
321
|
-
expect(response1.status).toBe(200)
|
|
322
|
-
expect(response2.status).toBe(429)
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
it('uses ipv4Prefix:32 so subnet keys are per-IP (no grouping)', async () => {
|
|
326
|
-
const router = new Router()
|
|
327
|
-
router.use(
|
|
328
|
-
rateLimit({
|
|
329
|
-
getUserId: async () => null, // force identity=ip so subnet layer is what we’re testing
|
|
330
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
331
|
-
baseWindowMs: 1000,
|
|
332
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
333
|
-
anonymousIpMultiplier: 1,
|
|
334
|
-
addRetryAfterHeader: false,
|
|
335
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
336
|
-
endpointLimits: [],
|
|
337
|
-
includeQueryInEndpointKey: false,
|
|
338
|
-
// subnet max = bucketMax * ipv4 scale = 5 * 0.2 = 1 request per /prefix per second
|
|
339
|
-
scales: { subnet: { ipv4: 0.2, ipv6: 10, ipv4Prefix: 32 } },
|
|
340
|
-
}),
|
|
341
|
-
)
|
|
342
|
-
router.add({
|
|
343
|
-
method: HttpMethod.GET,
|
|
344
|
-
path: '/ipv4-prefix',
|
|
345
|
-
handler: () => new Response('ok'),
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
const request = (ip: string) =>
|
|
349
|
-
new Request('https://example.com/ipv4-prefix', {
|
|
350
|
-
headers: { 'x-forwarded-for': ip },
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
// Different IPs => different /32 subnet keys => both allowed (each gets 1)
|
|
354
|
-
const r1 = await router.fetch(request('203.0.113.10'))
|
|
355
|
-
const r2 = await router.fetch(request('203.0.113.11'))
|
|
356
|
-
|
|
357
|
-
// Second request from same IP => same /32 subnet key => blocked
|
|
358
|
-
const r3 = await router.fetch(request('203.0.113.10'))
|
|
359
|
-
const r4 = await router.fetch(request('203.0.113.11'))
|
|
360
|
-
|
|
361
|
-
expect(r1.status).toBe(200)
|
|
362
|
-
expect(r2.status).toBe(200)
|
|
363
|
-
expect(r3.status).toBe(429)
|
|
364
|
-
expect(r4.status).toBe(429)
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
it('uses ipv4Prefix:16 so all IPs in the same /16 share a subnet bucket', async () => {
|
|
368
|
-
const router = new Router()
|
|
369
|
-
router.use(
|
|
370
|
-
rateLimit({
|
|
371
|
-
getUserId: async () => null, // force identity=ip; subnet layer is under test
|
|
372
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
373
|
-
baseWindowMs: 1000,
|
|
374
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
375
|
-
anonymousIpMultiplier: 1,
|
|
376
|
-
addRetryAfterHeader: false,
|
|
377
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
378
|
-
endpointLimits: [],
|
|
379
|
-
includeQueryInEndpointKey: false,
|
|
380
|
-
// subnet max = 5 * 0.2 = 1 request per /16 per second
|
|
381
|
-
scales: { subnet: { ipv4: 0.2, ipv6: 10, ipv4Prefix: 16 } },
|
|
382
|
-
}),
|
|
383
|
-
)
|
|
384
|
-
router.add({
|
|
385
|
-
method: HttpMethod.GET,
|
|
386
|
-
path: '/ipv4-prefix',
|
|
387
|
-
handler: () => new Response('ok'),
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
const request = (ip: string) =>
|
|
391
|
-
new Request('https://example.com/ipv4-prefix', {
|
|
392
|
-
headers: { 'x-forwarded-for': ip },
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
// Same /16: 203.0.0.0/16
|
|
396
|
-
const r1 = await router.fetch(request('203.0.113.10'))
|
|
397
|
-
const r2 = await router.fetch(request('203.0.200.42'))
|
|
398
|
-
|
|
399
|
-
// Different /16: 198.51.0.0/16
|
|
400
|
-
const r3 = await router.fetch(request('198.51.100.5'))
|
|
401
|
-
|
|
402
|
-
expect(r1.status).toBe(200)
|
|
403
|
-
// Second request from same /16 exceeds subnet bucket
|
|
404
|
-
expect(r2.status).toBe(429)
|
|
405
|
-
|
|
406
|
-
// Different /16 should not be affected
|
|
407
|
-
expect(r3.status).toBe(200)
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
it('uses custom ipv6Prefix for subnet grouping', async () => {
|
|
411
|
-
const router = new Router()
|
|
412
|
-
router.use(
|
|
413
|
-
rateLimit({
|
|
414
|
-
getUserId: async () => 'user-1',
|
|
415
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
416
|
-
baseWindowMs: 1000,
|
|
417
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
418
|
-
anonymousIpMultiplier: 1,
|
|
419
|
-
addRetryAfterHeader: false,
|
|
420
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
421
|
-
endpointLimits: [],
|
|
422
|
-
includeQueryInEndpointKey: false,
|
|
423
|
-
scales: { subnet: { ipv4: 10, ipv6: 0.2, ipv6Prefix: 128 } },
|
|
424
|
-
}),
|
|
425
|
-
)
|
|
426
|
-
router.add({
|
|
427
|
-
method: HttpMethod.GET,
|
|
428
|
-
path: '/ipv6-prefix',
|
|
429
|
-
handler: () => new Response('ok'),
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
const request = (ip: string) =>
|
|
433
|
-
new Request('https://example.com/ipv6-prefix', {
|
|
434
|
-
headers: { 'x-forwarded-for': ip },
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
const response1 = await router.fetch(request('2001:db8:abcd:12::1'))
|
|
438
|
-
const response2 = await router.fetch(request('2001:db8:abcd:12::2'))
|
|
439
|
-
const response3 = await router.fetch(request('2001:db8:abcd:12::1'))
|
|
440
|
-
|
|
441
|
-
expect(response1.status).toBe(200)
|
|
442
|
-
expect(response2.status).toBe(200)
|
|
443
|
-
expect(response3.status).toBe(429)
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
it('prefixes subnet keys to avoid collisions', async () => {
|
|
447
|
-
const storage = new CaptureStorage()
|
|
448
|
-
const router = new Router()
|
|
449
|
-
router.use(
|
|
450
|
-
rateLimit({
|
|
451
|
-
getUserId: async () => 'user-1',
|
|
452
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
453
|
-
baseWindowMs: 1000,
|
|
454
|
-
baseMaxRequestsPerBaseWindow: 1,
|
|
455
|
-
anonymousIpMultiplier: 1,
|
|
456
|
-
addRetryAfterHeader: false,
|
|
457
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
458
|
-
endpointLimits: [],
|
|
459
|
-
includeQueryInEndpointKey: false,
|
|
460
|
-
storage,
|
|
461
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
462
|
-
}),
|
|
463
|
-
)
|
|
464
|
-
router.add({
|
|
465
|
-
method: HttpMethod.GET,
|
|
466
|
-
path: '/subnet-prefix',
|
|
467
|
-
handler: () => new Response('ok'),
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
await router.fetch(
|
|
471
|
-
new Request('https://example.com/subnet-prefix', {
|
|
472
|
-
headers: { 'x-forwarded-for': '203.0.113.90' },
|
|
473
|
-
}),
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
expect(storage.keys.some((key) => key.startsWith('subnet:'))).toBe(true)
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
it('enforces country limits', async () => {
|
|
480
|
-
const router = new Router()
|
|
481
|
-
router.use(
|
|
482
|
-
rateLimit({
|
|
483
|
-
getUserId: async () => 'user-1',
|
|
484
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
485
|
-
baseWindowMs: 1000,
|
|
486
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
487
|
-
anonymousIpMultiplier: 1,
|
|
488
|
-
addRetryAfterHeader: false,
|
|
489
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
490
|
-
endpointLimits: [],
|
|
491
|
-
includeQueryInEndpointKey: false,
|
|
492
|
-
getCountryCode: async () => 'US',
|
|
493
|
-
scales: {
|
|
494
|
-
subnet: { ipv4: 10, ipv6: 10 },
|
|
495
|
-
country: { US: 0.2, other: 1, unknown: 1 },
|
|
496
|
-
},
|
|
497
|
-
}),
|
|
498
|
-
)
|
|
499
|
-
router.add({
|
|
500
|
-
method: HttpMethod.GET,
|
|
501
|
-
path: '/country',
|
|
502
|
-
handler: () => new Response('ok'),
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
const request = () =>
|
|
506
|
-
new Request('https://example.com/country', {
|
|
507
|
-
headers: { 'x-forwarded-for': '203.0.113.20' },
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
const response1 = await router.fetch(request())
|
|
511
|
-
const response2 = await router.fetch(request())
|
|
512
|
-
|
|
513
|
-
expect(response1.status).toBe(200)
|
|
514
|
-
expect(response2.status).toBe(429)
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
it('calls getGlobalPeakConcurrentUsers per request', async () => {
|
|
518
|
-
let calls = 0
|
|
519
|
-
const router = new Router()
|
|
520
|
-
router.use(
|
|
521
|
-
rateLimit({
|
|
522
|
-
getUserId: async () => 'user-1',
|
|
523
|
-
getGlobalPeakConcurrentUsers: async () => {
|
|
524
|
-
calls += 1
|
|
525
|
-
return 1
|
|
526
|
-
},
|
|
527
|
-
baseWindowMs: 1000,
|
|
528
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
529
|
-
anonymousIpMultiplier: 1,
|
|
530
|
-
addRetryAfterHeader: false,
|
|
531
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
532
|
-
endpointLimits: [],
|
|
533
|
-
includeQueryInEndpointKey: false,
|
|
534
|
-
getCountryCode: async () => 'US',
|
|
535
|
-
scales: {
|
|
536
|
-
subnet: { ipv4: 10, ipv6: 10 },
|
|
537
|
-
country: { US: 1, other: 1, unknown: 1 },
|
|
538
|
-
},
|
|
539
|
-
}),
|
|
540
|
-
)
|
|
541
|
-
router.add({
|
|
542
|
-
method: HttpMethod.GET,
|
|
543
|
-
path: '/peak',
|
|
544
|
-
handler: () => new Response('ok'),
|
|
545
|
-
})
|
|
546
|
-
|
|
547
|
-
const request = () =>
|
|
548
|
-
new Request('https://example.com/peak', {
|
|
549
|
-
headers: { 'x-forwarded-for': '203.0.113.21' },
|
|
550
|
-
})
|
|
551
|
-
|
|
552
|
-
const response1 = await router.fetch(request())
|
|
553
|
-
const response2 = await router.fetch(request())
|
|
554
|
-
|
|
555
|
-
expect(response1.status).toBe(200)
|
|
556
|
-
expect(response2.status).toBe(200)
|
|
557
|
-
expect(calls).toBe(2)
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
it('enforces ASN limits', async () => {
|
|
561
|
-
const router = new Router()
|
|
562
|
-
router.use(
|
|
563
|
-
rateLimit({
|
|
564
|
-
getUserId: async () => 'user-1',
|
|
565
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
566
|
-
baseWindowMs: 1000,
|
|
567
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
568
|
-
anonymousIpMultiplier: 1,
|
|
569
|
-
addRetryAfterHeader: false,
|
|
570
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
571
|
-
endpointLimits: [],
|
|
572
|
-
includeQueryInEndpointKey: false,
|
|
573
|
-
getAsn: async () => ({ asn: 15169, organization: 'Google' }),
|
|
574
|
-
scales: {
|
|
575
|
-
subnet: { ipv4: 10, ipv6: 10 },
|
|
576
|
-
asnClass: { cloud: 0.2, unknown: 1 },
|
|
577
|
-
},
|
|
578
|
-
}),
|
|
579
|
-
)
|
|
580
|
-
router.add({
|
|
581
|
-
method: HttpMethod.GET,
|
|
582
|
-
path: '/asn',
|
|
583
|
-
handler: () => new Response('ok'),
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
const request = () =>
|
|
587
|
-
new Request('https://example.com/asn', {
|
|
588
|
-
headers: { 'x-forwarded-for': '203.0.113.30' },
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
const response1 = await router.fetch(request())
|
|
592
|
-
const response2 = await router.fetch(request())
|
|
593
|
-
|
|
594
|
-
expect(response1.status).toBe(200)
|
|
595
|
-
expect(response2.status).toBe(429)
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
it.skipIf(!hasMaxmindDbs)('enforces ASN limits via MaxMind database', async () => {
|
|
599
|
-
const router = new Router()
|
|
600
|
-
router.use(
|
|
601
|
-
rateLimit({
|
|
602
|
-
getUserId: async () => 'user-1',
|
|
603
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
604
|
-
baseWindowMs: 10_000,
|
|
605
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
606
|
-
anonymousIpMultiplier: 1,
|
|
607
|
-
addRetryAfterHeader: false,
|
|
608
|
-
buckets: [{ windowMs: 10_000, scale: 1 }],
|
|
609
|
-
endpointLimits: [],
|
|
610
|
-
includeQueryInEndpointKey: false,
|
|
611
|
-
maxmindAsnDatabase: asnDbPath,
|
|
612
|
-
scales: {
|
|
613
|
-
subnet: { ipv4: 10, ipv6: 10 },
|
|
614
|
-
asnClass: { cloud: 0.2, unknown: 1 },
|
|
615
|
-
},
|
|
616
|
-
}),
|
|
617
|
-
)
|
|
618
|
-
router.add({
|
|
619
|
-
method: HttpMethod.GET,
|
|
620
|
-
path: '/asn-maxmind',
|
|
621
|
-
handler: () => new Response('ok'),
|
|
622
|
-
})
|
|
623
|
-
|
|
624
|
-
const request = () =>
|
|
625
|
-
new Request('https://example.com/asn-maxmind', {
|
|
626
|
-
headers: { 'x-forwarded-for': '8.8.8.8' },
|
|
627
|
-
})
|
|
628
|
-
|
|
629
|
-
const response1 = await router.fetch(request())
|
|
630
|
-
const response2 = await router.fetch(request())
|
|
631
|
-
|
|
632
|
-
expect(response1.status).toBe(200)
|
|
633
|
-
expect(response2.status).toBe(429)
|
|
634
|
-
})
|
|
635
|
-
|
|
636
|
-
it('uses a custom getIpAddress implementation', async () => {
|
|
637
|
-
const router = new Router()
|
|
638
|
-
router.use(
|
|
639
|
-
rateLimit({
|
|
640
|
-
getUserId: async () => null,
|
|
641
|
-
getIpAddress: async (ctx) => ctx.request.headers.get('x-test-ip') ?? 'unknown',
|
|
642
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
643
|
-
baseWindowMs: 1000,
|
|
644
|
-
baseMaxRequestsPerBaseWindow: 1,
|
|
645
|
-
anonymousIpMultiplier: 1,
|
|
646
|
-
addRetryAfterHeader: false,
|
|
647
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
648
|
-
endpointLimits: [],
|
|
649
|
-
includeQueryInEndpointKey: false,
|
|
650
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
651
|
-
}),
|
|
652
|
-
)
|
|
653
|
-
router.add({
|
|
654
|
-
method: HttpMethod.GET,
|
|
655
|
-
path: '/custom-ip',
|
|
656
|
-
handler: () => new Response('ok'),
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
const request = (ip: string) =>
|
|
660
|
-
new Request('https://example.com/custom-ip', {
|
|
661
|
-
headers: { 'x-test-ip': ip },
|
|
662
|
-
})
|
|
663
|
-
|
|
664
|
-
const response1 = await router.fetch(request('203.0.113.40'))
|
|
665
|
-
const response2 = await router.fetch(request('198.51.100.42'))
|
|
666
|
-
|
|
667
|
-
expect(response1.status).toBe(200)
|
|
668
|
-
expect(response2.status).toBe(200)
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
it.skipIf(!hasMaxmindDbs)('enforces country limits via MaxMind database', async () => {
|
|
672
|
-
const router = new Router()
|
|
673
|
-
router.use(
|
|
674
|
-
rateLimit({
|
|
675
|
-
getUserId: async () => 'user-1',
|
|
676
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
677
|
-
baseWindowMs: 1000,
|
|
678
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
679
|
-
anonymousIpMultiplier: 1,
|
|
680
|
-
addRetryAfterHeader: false,
|
|
681
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
682
|
-
endpointLimits: [],
|
|
683
|
-
includeQueryInEndpointKey: false,
|
|
684
|
-
maxmindCountryDatabase: countryDbPath,
|
|
685
|
-
scales: {
|
|
686
|
-
subnet: { ipv4: 10, ipv6: 10 },
|
|
687
|
-
country: { US: 0.2, other: 1, unknown: 1 },
|
|
688
|
-
},
|
|
689
|
-
}),
|
|
690
|
-
)
|
|
691
|
-
router.add({
|
|
692
|
-
method: HttpMethod.GET,
|
|
693
|
-
path: '/country-maxmind',
|
|
694
|
-
handler: () => new Response('ok'),
|
|
695
|
-
})
|
|
696
|
-
|
|
697
|
-
const request = () =>
|
|
698
|
-
new Request('https://example.com/country-maxmind', {
|
|
699
|
-
headers: { 'x-forwarded-for': '8.8.8.8' },
|
|
700
|
-
})
|
|
701
|
-
|
|
702
|
-
const response1 = await router.fetch(request())
|
|
703
|
-
const response2 = await router.fetch(request())
|
|
704
|
-
|
|
705
|
-
expect(response1.status).toBe(200)
|
|
706
|
-
expect(response2.status).toBe(429)
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
it.skipIf(!hasMaxmindDbs)('falls back to registered country from MaxMind', async () => {
|
|
710
|
-
const router = new Router()
|
|
711
|
-
router.use(
|
|
712
|
-
rateLimit({
|
|
713
|
-
getUserId: async () => 'user-1',
|
|
714
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
715
|
-
baseWindowMs: 1000,
|
|
716
|
-
baseMaxRequestsPerBaseWindow: 5,
|
|
717
|
-
anonymousIpMultiplier: 1,
|
|
718
|
-
addRetryAfterHeader: false,
|
|
719
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
720
|
-
endpointLimits: [],
|
|
721
|
-
includeQueryInEndpointKey: false,
|
|
722
|
-
maxmindCountryDatabase: countryDbPath,
|
|
723
|
-
scales: {
|
|
724
|
-
subnet: { ipv4: 10, ipv6: 10 },
|
|
725
|
-
country: { AU: 0.2, other: 1, unknown: 1 },
|
|
726
|
-
},
|
|
727
|
-
}),
|
|
728
|
-
)
|
|
729
|
-
router.add({
|
|
730
|
-
method: HttpMethod.GET,
|
|
731
|
-
path: '/country-registered',
|
|
732
|
-
handler: () => new Response('ok'),
|
|
733
|
-
})
|
|
734
|
-
|
|
735
|
-
const request = () =>
|
|
736
|
-
new Request('https://example.com/country-registered', {
|
|
737
|
-
headers: { 'x-forwarded-for': '1.1.1.1' },
|
|
738
|
-
})
|
|
739
|
-
|
|
740
|
-
const response1 = await router.fetch(request())
|
|
741
|
-
const response2 = await router.fetch(request())
|
|
742
|
-
|
|
743
|
-
expect(response1.status).toBe(200)
|
|
744
|
-
expect(response2.status).toBe(429)
|
|
745
|
-
})
|
|
746
|
-
|
|
747
|
-
it('applies non-default baseWindowMs and anonymousIpMultiplier', async () => {
|
|
748
|
-
const router = new Router()
|
|
749
|
-
router.use(
|
|
750
|
-
rateLimit({
|
|
751
|
-
getUserId: async () => null,
|
|
752
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
753
|
-
baseWindowMs: 2000,
|
|
754
|
-
baseMaxRequestsPerBaseWindow: 2,
|
|
755
|
-
anonymousIpMultiplier: 0.5,
|
|
756
|
-
addRetryAfterHeader: false,
|
|
757
|
-
buckets: [{ windowMs: 2000, scale: 1 }],
|
|
758
|
-
endpointLimits: [],
|
|
759
|
-
includeQueryInEndpointKey: false,
|
|
760
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
761
|
-
}),
|
|
762
|
-
)
|
|
763
|
-
router.add({
|
|
764
|
-
method: HttpMethod.GET,
|
|
765
|
-
path: '/anon',
|
|
766
|
-
handler: () => new Response('ok'),
|
|
767
|
-
})
|
|
768
|
-
|
|
769
|
-
const request = () =>
|
|
770
|
-
new Request('https://example.com/anon', {
|
|
771
|
-
headers: { 'x-forwarded-for': '203.0.113.50' },
|
|
772
|
-
})
|
|
773
|
-
|
|
774
|
-
const response1 = await router.fetch(request())
|
|
775
|
-
const response2 = await router.fetch(request())
|
|
776
|
-
|
|
777
|
-
expect(response1.status).toBe(200)
|
|
778
|
-
expect(response2.status).toBe(429)
|
|
779
|
-
})
|
|
780
|
-
|
|
781
|
-
it('does not expire in-memory counters before the window resets', async () => {
|
|
782
|
-
const router = new Router()
|
|
783
|
-
router.use(
|
|
784
|
-
rateLimit({
|
|
785
|
-
getUserId: async () => null,
|
|
786
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
787
|
-
baseWindowMs: 10_000,
|
|
788
|
-
baseMaxRequestsPerBaseWindow: 1,
|
|
789
|
-
anonymousIpMultiplier: 1,
|
|
790
|
-
addRetryAfterHeader: false,
|
|
791
|
-
buckets: [{ windowMs: 10_000, scale: 1 }],
|
|
792
|
-
endpointLimits: [],
|
|
793
|
-
includeQueryInEndpointKey: false,
|
|
794
|
-
inMemory: { ttlMs: 0 },
|
|
795
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
796
|
-
}),
|
|
797
|
-
)
|
|
798
|
-
router.add({
|
|
799
|
-
method: HttpMethod.GET,
|
|
800
|
-
path: '/ttl',
|
|
801
|
-
handler: () => new Response('ok'),
|
|
802
|
-
})
|
|
803
|
-
|
|
804
|
-
const request = () =>
|
|
805
|
-
new Request('https://example.com/ttl', {
|
|
806
|
-
headers: { 'x-forwarded-for': '203.0.113.100' },
|
|
807
|
-
})
|
|
808
|
-
|
|
809
|
-
const response1 = await router.fetch(request())
|
|
810
|
-
await delay(30)
|
|
811
|
-
const response2 = await router.fetch(request())
|
|
812
|
-
|
|
813
|
-
expect(response1.status).toBe(200)
|
|
814
|
-
expect(response2.status).toBe(429)
|
|
815
|
-
})
|
|
816
|
-
|
|
817
|
-
it('enforces multiple buckets', async () => {
|
|
818
|
-
const router = new Router()
|
|
819
|
-
router.use(
|
|
820
|
-
rateLimit({
|
|
821
|
-
getUserId: async () => 'user-1',
|
|
822
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
823
|
-
baseWindowMs: 1000,
|
|
824
|
-
baseMaxRequestsPerBaseWindow: 3,
|
|
825
|
-
anonymousIpMultiplier: 1,
|
|
826
|
-
addRetryAfterHeader: false,
|
|
827
|
-
buckets: [
|
|
828
|
-
{ windowMs: 1000, scale: 1 },
|
|
829
|
-
{ windowMs: 4000, scale: 0.1 },
|
|
830
|
-
],
|
|
831
|
-
endpointLimits: [],
|
|
832
|
-
includeQueryInEndpointKey: false,
|
|
833
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
834
|
-
}),
|
|
835
|
-
)
|
|
836
|
-
router.add({
|
|
837
|
-
method: HttpMethod.GET,
|
|
838
|
-
path: '/buckets',
|
|
839
|
-
handler: () => new Response('ok'),
|
|
840
|
-
})
|
|
841
|
-
|
|
842
|
-
const request = () =>
|
|
843
|
-
new Request('https://example.com/buckets', {
|
|
844
|
-
headers: { 'x-forwarded-for': '203.0.113.60' },
|
|
845
|
-
})
|
|
846
|
-
|
|
847
|
-
const response1 = await router.fetch(request())
|
|
848
|
-
const response2 = await router.fetch(request())
|
|
849
|
-
|
|
850
|
-
expect(response1.status).toBe(200)
|
|
851
|
-
expect(response2.status).toBe(429)
|
|
852
|
-
})
|
|
853
|
-
|
|
854
|
-
it('omits query separator when the query is empty', async () => {
|
|
855
|
-
const storage = new CaptureStorage()
|
|
856
|
-
const router = new Router()
|
|
857
|
-
router.use(
|
|
858
|
-
rateLimit({
|
|
859
|
-
getUserId: async () => null,
|
|
860
|
-
getGlobalPeakConcurrentUsers: async () => 1,
|
|
861
|
-
baseWindowMs: 1000,
|
|
862
|
-
baseMaxRequestsPerBaseWindow: 1,
|
|
863
|
-
anonymousIpMultiplier: 1,
|
|
864
|
-
addRetryAfterHeader: false,
|
|
865
|
-
buckets: [{ windowMs: 1000, scale: 1 }],
|
|
866
|
-
endpointLimits: [{ pattern: '/empty-query', limit: { GET: 1 } }],
|
|
867
|
-
includeQueryInEndpointKey: true,
|
|
868
|
-
storage,
|
|
869
|
-
scales: { subnet: { ipv4: 10, ipv6: 10 } },
|
|
870
|
-
}),
|
|
871
|
-
)
|
|
872
|
-
router.add({
|
|
873
|
-
method: HttpMethod.GET,
|
|
874
|
-
path: '/empty-query',
|
|
875
|
-
handler: () => new Response('ok'),
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
await router.fetch(
|
|
879
|
-
new Request('https://example.com/empty-query', {
|
|
880
|
-
headers: { 'x-forwarded-for': '203.0.113.110' },
|
|
881
|
-
}),
|
|
882
|
-
)
|
|
883
|
-
|
|
884
|
-
expect(storage.keys.some((key) => key.includes('routeq:GET:/empty-query?'))).toBe(false)
|
|
885
|
-
})
|
|
886
|
-
})
|