@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.
Files changed (133) hide show
  1. package/dist/bin.d.mts +4 -0
  2. package/dist/client/react.d.mts +178 -0
  3. package/dist/client/react.mjs +142 -0
  4. package/dist/client.d.mts +433 -0
  5. package/dist/client.mjs +264 -0
  6. package/dist/content-BuDOmhH_.mjs +102 -0
  7. package/dist/core-CzUCxvGk.d.mts +140 -0
  8. package/dist/core-DbmQauwS.mjs +81 -0
  9. package/dist/handlers.d.mts +72 -0
  10. package/dist/handlers.mjs +153 -0
  11. package/dist/index.d.mts +3 -0
  12. package/dist/index.mjs +1152 -0
  13. package/dist/middleware.d.mts +388 -0
  14. package/dist/middleware.mjs +1222 -0
  15. package/dist/request-Dn0zc-xm.mjs +1025 -0
  16. package/dist/response/content.d.mts +79 -0
  17. package/dist/response/content.mjs +2 -0
  18. package/dist/response/json-rpc.d.mts +1 -0
  19. package/dist/response/json-rpc.mjs +1 -0
  20. package/dist/response/problem/valibot.d.mts +230 -0
  21. package/dist/response/problem/valibot.mjs +258 -0
  22. package/dist/response/problem.d.mts +415 -0
  23. package/dist/response/problem.mjs +183 -0
  24. package/dist/response/status.d.mts +45 -0
  25. package/dist/response/status.mjs +2 -0
  26. package/dist/responses-B379Ep9Y.d.mts +296 -0
  27. package/dist/responses-BpVrgeYi.mjs +101 -0
  28. package/dist/router-Cwb7ak0J.d.mts +1819 -0
  29. package/dist/routes.d.mts +282 -0
  30. package/dist/routes.mjs +311 -0
  31. package/dist/status-C-8mw-FB.mjs +59 -0
  32. package/dist/valibot-D7liFYyB.d.mts +290 -0
  33. package/dist/valibot-Du97X-TS.mjs +326 -0
  34. package/package.json +8 -2
  35. package/src/bin/gen-api-client.test.ts +0 -70
  36. package/src/bin/gen-api-client.ts +0 -986
  37. package/src/client/headers.ts +0 -31
  38. package/src/client/index.ts +0 -8
  39. package/src/client/promise.ts +0 -11
  40. package/src/client/react/index.test.tsx +0 -266
  41. package/src/client/react/index.ts +0 -431
  42. package/src/client/responses.test.ts +0 -151
  43. package/src/client/responses.ts +0 -278
  44. package/src/client/transport.ts +0 -74
  45. package/src/client/transports/body-codec.ts +0 -61
  46. package/src/client/transports/fetch.ts +0 -113
  47. package/src/client/tsconfig.json +0 -9
  48. package/src/client/types.ts +0 -15
  49. package/src/client/url.ts +0 -31
  50. package/src/index.ts +0 -63
  51. package/src/router/fetch-types.ts +0 -13
  52. package/src/router/handlers/index.ts +0 -2
  53. package/src/router/handlers/openapi/index.ts +0 -2
  54. package/src/router/handlers/openapi/openapi.ts +0 -293
  55. package/src/router/integration/zod-openapi.test.ts +0 -74
  56. package/src/router/lib/charset.test.ts +0 -22
  57. package/src/router/lib/charset.ts +0 -133
  58. package/src/router/lib/collections.ts +0 -3
  59. package/src/router/lib/format.test.ts +0 -67
  60. package/src/router/lib/format.ts +0 -35
  61. package/src/router/lib/host.ts +0 -4
  62. package/src/router/lib/json-schema.ts +0 -6
  63. package/src/router/lib/media-type.test.ts +0 -122
  64. package/src/router/lib/media-type.ts +0 -289
  65. package/src/router/lib/pathname.test.ts +0 -18
  66. package/src/router/lib/pathname.ts +0 -19
  67. package/src/router/lib/route-names.ts +0 -70
  68. package/src/router/lib/route-normalize.test.ts +0 -36
  69. package/src/router/lib/route-normalize.ts +0 -67
  70. package/src/router/lib/schema-merge.ts +0 -56
  71. package/src/router/middleware/accept-ctx.test.ts +0 -33
  72. package/src/router/middleware/accept-ctx.ts +0 -12
  73. package/src/router/middleware/body-limit.test.ts +0 -112
  74. package/src/router/middleware/body-limit.ts +0 -121
  75. package/src/router/middleware/content-type-context.ts +0 -0
  76. package/src/router/middleware/cors.test.ts +0 -269
  77. package/src/router/middleware/cors.ts +0 -490
  78. package/src/router/middleware/csrf.test.ts +0 -106
  79. package/src/router/middleware/csrf.ts +0 -192
  80. package/src/router/middleware/define.ts +0 -249
  81. package/src/router/middleware/index.ts +0 -34
  82. package/src/router/middleware/jsxhtml-response.ts +0 -0
  83. package/src/router/middleware/oas-swagger.ts +0 -0
  84. package/src/router/middleware/rate-limit.test.ts +0 -886
  85. package/src/router/middleware/rate-limit.ts +0 -920
  86. package/src/router/middleware/request-id-ctx.test.ts +0 -183
  87. package/src/router/middleware/request-id-ctx.ts +0 -135
  88. package/src/router/middleware/request-logger-format.test.ts +0 -16
  89. package/src/router/middleware/request-logger-format.ts +0 -269
  90. package/src/router/middleware/request-logger.test.ts +0 -267
  91. package/src/router/middleware/request-logger.ts +0 -131
  92. package/src/router/middleware/start-time-ctx.ts +0 -5
  93. package/src/router/request.ts +0 -611
  94. package/src/router/response/core.ts +0 -181
  95. package/src/router/response/directives.ts +0 -233
  96. package/src/router/response/formats/content/bodyless.ts +0 -54
  97. package/src/router/response/formats/content/content.ts +0 -79
  98. package/src/router/response/formats/content/index.ts +0 -2
  99. package/src/router/response/formats/json-rpc/index.ts +0 -2
  100. package/src/router/response/formats/problem/badRequest.ts +0 -90
  101. package/src/router/response/formats/problem/conflict.ts +0 -90
  102. package/src/router/response/formats/problem/created.ts +0 -40
  103. package/src/router/response/formats/problem/index.ts +0 -27
  104. package/src/router/response/formats/problem/notFound.ts +0 -90
  105. package/src/router/response/formats/problem/permissionDenied.ts +0 -90
  106. package/src/router/response/formats/problem/problem.test.ts +0 -888
  107. package/src/router/response/formats/problem/rateLimited.ts +0 -90
  108. package/src/router/response/formats/problem/responses.ts +0 -219
  109. package/src/router/response/formats/problem/root-errors.ts +0 -48
  110. package/src/router/response/formats/problem/sessionExpired.ts +0 -90
  111. package/src/router/response/formats/problem/types.ts +0 -170
  112. package/src/router/response/formats/problem/unauthenticated.ts +0 -90
  113. package/src/router/response/formats/problem/valibot.ts +0 -410
  114. package/src/router/response/formats/status/index.ts +0 -1
  115. package/src/router/response/formats/status/responses.ts +0 -59
  116. package/src/router/response/formats/status/status.test.ts +0 -21
  117. package/src/router/response/framers.ts +0 -85
  118. package/src/router/response/index.ts +0 -28
  119. package/src/router/response/openapi.test.ts +0 -96
  120. package/src/router/response/openapi.ts +0 -1
  121. package/src/router/response/serializers.ts +0 -66
  122. package/src/router/response/stream.ts +0 -35
  123. package/src/router/router.test.ts +0 -1571
  124. package/src/router/router.ts +0 -1965
  125. package/src/router/routes/index.ts +0 -46
  126. package/src/router/routes/valibot/index.ts +0 -18
  127. package/src/router/routes/valibot/valibot.ts +0 -1393
  128. package/src/router/routes/valibot.test.ts +0 -286
  129. package/src/router/routes/zod/index.ts +0 -18
  130. package/src/router/routes/zod/zod.ts +0 -1318
  131. package/src/router/routes/zod.test.ts +0 -280
  132. package/src/router/server-interface.ts +0 -31
  133. package/src/router/types.ts +0 -657
@@ -0,0 +1,1222 @@
1
+ import { C as isChunkDirective, O as isStreamDirective, R as parseAcceptHeader, S as headers, T as isHeadersDirective, f as defineMiddleware, n as RequestBodyLengthMismatchError, r as RequestBodyTooLargeError, w as isHeadDirective } from "./request-Dn0zc-xm.mjs";
2
+ import { a as response, i as isRoutekitResponse, n as isResponseBodyInit } from "./core-DbmQauwS.mjs";
3
+ import { a as text, t as empty } from "./content-BuDOmhH_.mjs";
4
+ import { CommonHeaders, HttpMethod, HttpStatus } from "@mpen/http";
5
+ import { LogLevel } from "@mpen/logger";
6
+ //#region src/router/middleware/request-id-ctx.ts
7
+ globalThis._reloadCounter = globalThis._reloadCounter == null ? 0 : globalThis._reloadCounter + 1;
8
+ globalThis._globalRequestCounter ??= 0;
9
+ const isHMR = import.meta.hot !== void 0 || process.execArgv.includes("--hot");
10
+ function defaultRequestIdGenerator(_ctx, extra) {
11
+ let requestId = extra.requestCounter.toString(36);
12
+ if (isHMR) requestId = extra.hotReloadCounter.toString(36) + "." + requestId;
13
+ if (extra.prefix) requestId = `${extra.prefix}.${requestId}`;
14
+ return requestId;
15
+ }
16
+ /**
17
+ * Attach a correlated request id to every final response and contextual logger record.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * router.useRequest(requestIdCtx({
22
+ * readHeaderName: ['x-request-id', 'x-trace-id'],
23
+ * writeHeaderName: 'x-request-id',
24
+ * generate: () => crypto.randomUUID(),
25
+ * }))
26
+ * ```
27
+ *
28
+ * @param options - Configuration for reading, generating, and writing request ids.
29
+ * @returns Request-boundary middleware that populates `requestId` and logger context.
30
+ * @typeParam Ctx - Context available before request id generation.
31
+ */
32
+ function requestIdCtx(options = {}) {
33
+ const prefix = options.prefix ?? "";
34
+ const headers = options.readHeaderName === void 0 ? [
35
+ "x-request-id",
36
+ "x-trace-id",
37
+ "traceparent"
38
+ ] : options.readHeaderName == null ? [] : Array.isArray(options.readHeaderName) ? options.readHeaderName : [options.readHeaderName];
39
+ const writeHeaderName = options.writeHeaderName;
40
+ const hotReloadCounter = globalThis._reloadCounter;
41
+ let requestCounter = 0;
42
+ return async (ctx, next) => {
43
+ let headerId = null;
44
+ for (const name of headers) {
45
+ headerId = ctx.request.headers.get(name);
46
+ if (headerId !== null) break;
47
+ }
48
+ const extra = {
49
+ prefix,
50
+ hotReloadCounter,
51
+ requestCounter: ++requestCounter,
52
+ globalRequestCounter: ++globalThis._globalRequestCounter
53
+ };
54
+ const requestId = headerId ?? options.generate?.(ctx, extra) ?? defaultRequestIdGenerator(ctx, extra);
55
+ ctx.requestId = requestId;
56
+ ctx.logger = ctx.logger.withContext({ "request.id": requestId });
57
+ const response = await next();
58
+ if (writeHeaderName != null) response.headers.set(writeHeaderName, requestId);
59
+ return response;
60
+ };
61
+ }
62
+ //#endregion
63
+ //#region src/router/middleware/request-logger-format.ts
64
+ const ROUTEKIT_HTTP_SERVER_REQUEST_EVENT_NAME = "http.server.request";
65
+ const ROUTEKIT_REQUEST_CONTEXT_KEYS = [
66
+ "http.request.method",
67
+ "url.path",
68
+ "request.id"
69
+ ];
70
+ const REQUEST_ID_COLOR_KEYS = [
71
+ "cyanBright",
72
+ "magentaBright",
73
+ "greenBright",
74
+ "yellowBright",
75
+ "blueBright",
76
+ "redBright",
77
+ "cyan",
78
+ "magenta"
79
+ ];
80
+ /**
81
+ * Transform Routekit request activity records into production JSON access logs.
82
+ *
83
+ * Removes the fallback activity message from `http.server.request` records while preserving
84
+ * normal application log messages written through the same contextual logger.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { JsonLogger } from '@mpen/logger'
89
+ * import { transformRoutekitJsonLogRecord } from '@mpen/routekit/middleware'
90
+ *
91
+ * const logger = new JsonLogger({
92
+ * transformRecord: transformRoutekitJsonLogRecord,
93
+ * })
94
+ * ```
95
+ *
96
+ * @param record - Record emitted by a logger.
97
+ * @returns The transformed record, or `undefined` to use the original record.
98
+ */
99
+ const transformRoutekitJsonLogRecord = (record) => {
100
+ if (!isRoutekitRequestEvent(record)) return;
101
+ return {
102
+ ...record,
103
+ data: stripActivityLifecycleMessage(record)
104
+ };
105
+ };
106
+ /**
107
+ * Format Routekit request context for terminal logs.
108
+ *
109
+ * Renders compact request start/completion lines, color-correlated request identifiers, status
110
+ * codes, elapsed time, and response sizes while leaving unrelated records to
111
+ * [`TerminalLogger`]{@link import('@mpen/logger').TerminalLogger}'s default renderer.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * import { TerminalLogger } from '@mpen/logger'
116
+ * import { formatRoutekitTerminalLogRecord } from '@mpen/routekit/middleware'
117
+ *
118
+ * const logger = new TerminalLogger({
119
+ * formatRecord: formatRoutekitTerminalLogRecord,
120
+ * })
121
+ * ```
122
+ *
123
+ * @param record - Record emitted by a logger.
124
+ * @param terminal - Terminal formatting helpers.
125
+ * @returns Formatted terminal output, or `undefined` to use the default renderer.
126
+ */
127
+ const formatRoutekitTerminalLogRecord = (record, terminal) => {
128
+ if (!hasRoutekitRequestContext(record)) return;
129
+ const colors = terminal.colors;
130
+ const context = record.context;
131
+ const requestId = getStringContext(record, "request.id");
132
+ const method = getStringContext(record, "http.request.method");
133
+ const path = getStringContext(record, "url.path");
134
+ const route = getStringContext(record, "http.route");
135
+ const direction = getActivityDirection(record);
136
+ const parts = [
137
+ formatServiceName(getStringContext(record, "service.name"), colors),
138
+ requestId == null ? void 0 : formatRequestId(requestId, colors),
139
+ requestId == null ? void 0 : formatDirection(direction, colors),
140
+ requestId == null ? formatDirection(direction, colors) : void 0,
141
+ colors.bold(colors.whiteBright(method)),
142
+ colors.white(path),
143
+ route == null || route === path ? void 0 : colors.blackBright(`route=${route}`),
144
+ formatStatusCode(getNumberContext(context["http.response.status_code"]), colors),
145
+ formatDuration(getNumberContext(context.duration_ms), colors),
146
+ formatBodySize(getNumberContext(context["http.response.body.size"]), colors)
147
+ ].filter((part) => part != null && part !== "");
148
+ const prefix = `${terminal.icon} ${colors.blackBright(terminal.time)} ${parts.join(" ")}`;
149
+ const data = direction == null ? record.data : stripActivityLifecycleMessage(record);
150
+ if (data.length === 0) return prefix.trimEnd();
151
+ const message = terminal.formatData(data);
152
+ if (message === "") return prefix.trimEnd();
153
+ const [firstLine = "", ...remainingLines] = message.split("\n");
154
+ return [prefix + " " + firstLine, ...remainingLines.map((line) => " " + line)].join("\n");
155
+ };
156
+ function hasRoutekitRequestContext(record) {
157
+ return ROUTEKIT_REQUEST_CONTEXT_KEYS.every((key) => record.context[key] != null);
158
+ }
159
+ function isRoutekitRequestEvent(record) {
160
+ return record.context["event.name"] === ROUTEKIT_HTTP_SERVER_REQUEST_EVENT_NAME;
161
+ }
162
+ function stripActivityLifecycleMessage(record) {
163
+ return record.metadata.activity == null ? record.data : record.data.slice(1);
164
+ }
165
+ function getActivityDirection(record) {
166
+ switch (record.metadata.activity?.phase) {
167
+ case "start": return "→";
168
+ case "end": return "←";
169
+ }
170
+ }
171
+ function getStringContext(record, key) {
172
+ const value = record.context[key];
173
+ return value == null ? void 0 : String(value);
174
+ }
175
+ function getNumberContext(value) {
176
+ if (typeof value !== "number") return;
177
+ return Number.isFinite(value) ? value : void 0;
178
+ }
179
+ function formatServiceName(value, colors) {
180
+ if (value == null) return;
181
+ const text = `[${value}]`;
182
+ return colors.bold(colors.blueBright(text));
183
+ }
184
+ function formatRequestId(value, colors) {
185
+ const text = `[${value}]`;
186
+ let hash = 0;
187
+ for (const char of value) hash = hash * 31 + char.codePointAt(0) >>> 0;
188
+ return colors[REQUEST_ID_COLOR_KEYS[hash % REQUEST_ID_COLOR_KEYS.length]](text);
189
+ }
190
+ function formatDirection(value, colors) {
191
+ return value == null ? void 0 : colors.bold(colors.whiteBright(value));
192
+ }
193
+ function formatStatusCode(value, colors) {
194
+ if (value == null) return;
195
+ const text = String(value);
196
+ const formatted = value >= 500 ? colors.redBright(text) : value >= 400 ? colors.yellowBright(text) : value >= 300 ? colors.cyanBright(text) : colors.greenBright(text);
197
+ return colors.bold(formatted);
198
+ }
199
+ function formatDuration(value, colors) {
200
+ if (value == null) return;
201
+ const text = `${formatDurationMilliseconds(value)}ms`;
202
+ if (value >= 1e3) return colors.redBright(text);
203
+ return value >= 250 ? colors.yellowBright(text) : colors.greenBright(text);
204
+ }
205
+ /**
206
+ * @internal
207
+ */
208
+ function formatDurationMilliseconds(value) {
209
+ const p2 = value.toFixed(2);
210
+ if (Number(p2) < 1) return p2;
211
+ const p1 = value.toFixed(1);
212
+ if (Number(p1) < 100) return p1;
213
+ return value.toFixed(0);
214
+ }
215
+ function formatBodySize(value, colors) {
216
+ return value == null ? void 0 : colors.cyan(formatByteSize(value));
217
+ }
218
+ function formatByteSize(value) {
219
+ if (!Number.isFinite(value) || value < 0) return String(value);
220
+ if (value < 1024) return `${value} B`;
221
+ const units = [
222
+ "KiB",
223
+ "MiB",
224
+ "GiB"
225
+ ];
226
+ let amount = value;
227
+ let unit = "B";
228
+ for (const nextUnit of units) {
229
+ amount /= 1024;
230
+ unit = nextUnit;
231
+ if (amount < 1024 || nextUnit === units.at(-1)) break;
232
+ }
233
+ const precision = amount >= 100 ? 0 : amount >= 10 ? 1 : 2;
234
+ return `${amount.toFixed(precision)} ${unit}`;
235
+ }
236
+ //#endregion
237
+ //#region src/router/middleware/request-logger.ts
238
+ function defaultCompletionLevel(response) {
239
+ return response.status >= 500 ? LogLevel.ERROR : LogLevel.INFO;
240
+ }
241
+ const HTTP_SERVER_REQUEST_EVENT = { "event.name": ROUTEKIT_HTTP_SERVER_REQUEST_EVENT_NAME };
242
+ function getResponseBodySize(response) {
243
+ if (response.body == null) return 0;
244
+ const value = response.headers.get("content-length");
245
+ if (value == null || !/^\d+$/u.test(value)) return;
246
+ const size = Number(value);
247
+ return Number.isSafeInteger(size) ? size : void 0;
248
+ }
249
+ function getClientAddress(request, trustedHeader) {
250
+ if (trustedHeader == null) return;
251
+ const value = request.headers.get(trustedHeader)?.trim();
252
+ if (!value) return;
253
+ return trustedHeader.toLowerCase() === "x-forwarded-for" ? value.split(",", 1)[0]?.trim() : value;
254
+ }
255
+ /**
256
+ * Log a timed request activity through the request's contextual logger.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * router.useRequest(requestIdCtx())
261
+ * router.useRequest(requestLogger())
262
+ * ```
263
+ *
264
+ * @param options - Request activity naming and severity options.
265
+ * @returns Request-boundary middleware that logs final status and duration.
266
+ * @typeParam Ctx - Context containing the correlated request identifier.
267
+ */
268
+ function requestLogger(options = {}) {
269
+ const activityName = options.activityName ?? "request";
270
+ return async (ctx, next) => {
271
+ const clientAddress = getClientAddress(ctx.request, options.trustedClientAddressHeader);
272
+ const userAgent = ctx.request.headers.get("user-agent") ?? void 0;
273
+ const activity = ctx.logger.startActivity(activityName, {
274
+ ...clientAddress == null ? {} : { "client.address": clientAddress },
275
+ ...userAgent == null ? {} : { "user_agent.original": userAgent }
276
+ });
277
+ ctx.logger = activity.logger;
278
+ try {
279
+ const response = await next();
280
+ const bodySize = getResponseBodySize(response);
281
+ const level = typeof options.completionLevel === "function" ? options.completionLevel(response) : options.completionLevel ?? defaultCompletionLevel(response);
282
+ activity.end({
283
+ context: {
284
+ ...HTTP_SERVER_REQUEST_EVENT,
285
+ "http.response.status_code": response.status,
286
+ ...bodySize == null ? {} : { "http.response.body.size": bodySize }
287
+ },
288
+ level
289
+ });
290
+ return response;
291
+ } catch (error) {
292
+ activity.end({
293
+ context: HTTP_SERVER_REQUEST_EVENT,
294
+ data: [error],
295
+ level: LogLevel.ERROR
296
+ });
297
+ throw error;
298
+ }
299
+ };
300
+ }
301
+ //#endregion
302
+ //#region src/router/middleware/body-limit.ts
303
+ const utf8encoder = new TextEncoder();
304
+ function parseContentLength(value) {
305
+ if (!value) return null;
306
+ const trimmed = value.trim();
307
+ if (!trimmed) return null;
308
+ const parsed = Number(trimmed);
309
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) return null;
310
+ return parsed;
311
+ }
312
+ function chunkByteLength(chunk) {
313
+ if (typeof chunk === "string") return utf8encoder.encode(chunk).length;
314
+ return chunk.byteLength;
315
+ }
316
+ /**
317
+ * Enforce a maximum request body size while preserving access to the incoming stream.
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * router.use(bodyLimit({maxSize: 1024 * 1024}))
322
+ * ```
323
+ *
324
+ * @param options - Configuration for maximum request body size enforcement.
325
+ * @returns Middleware that rejects oversized bodies and mismatched Content-Length values.
326
+ */
327
+ function bodyLimit(options) {
328
+ const textResponse = {
329
+ schema: { type: "string" },
330
+ parse(value) {
331
+ if (typeof value !== "string") throw new TypeError("Body limit responses must contain a string body.");
332
+ return value;
333
+ }
334
+ };
335
+ return defineMiddleware({
336
+ responses: {
337
+ [HttpStatus.BAD_REQUEST]: textResponse,
338
+ [HttpStatus.PAYLOAD_TOO_LARGE]: textResponse
339
+ },
340
+ async run(ctx, { next, forward, respond }) {
341
+ const maxSize = options.maxSize;
342
+ const contentLength = parseContentLength(ctx.request.headers.get("content-length"));
343
+ if (contentLength != null && contentLength > maxSize) {
344
+ ctx.request.body.stream()?.cancel();
345
+ return respond(text("Payload Too Large", { status: HttpStatus.PAYLOAD_TOO_LARGE }));
346
+ }
347
+ const bodyStream = ctx.request.body.stream();
348
+ if (!bodyStream) {
349
+ if (contentLength != null && contentLength !== 0) return respond(text("Bad Request", { status: HttpStatus.BAD_REQUEST }));
350
+ return forward(await next());
351
+ }
352
+ const reader = bodyStream.getReader();
353
+ let bytesRead = 0;
354
+ const monitoredBody = new ReadableStream({
355
+ async pull(controller) {
356
+ const result = await reader.read();
357
+ if (result.done) {
358
+ if (contentLength != null && bytesRead !== contentLength) {
359
+ controller.error(new RequestBodyLengthMismatchError(contentLength, bytesRead));
360
+ return;
361
+ }
362
+ controller.close();
363
+ return;
364
+ }
365
+ const value = result.value;
366
+ bytesRead += chunkByteLength(value);
367
+ if (bytesRead > maxSize) {
368
+ await reader.cancel();
369
+ controller.error(new RequestBodyTooLargeError(maxSize, bytesRead));
370
+ return;
371
+ }
372
+ controller.enqueue(value);
373
+ },
374
+ cancel(reason) {
375
+ return reader.cancel(reason);
376
+ }
377
+ });
378
+ ctx.request = ctx.request.withBody(ctx.request.body.withStream(monitoredBody));
379
+ try {
380
+ return forward(await next());
381
+ } catch (err) {
382
+ if (err instanceof RequestBodyTooLargeError) return respond(text("Payload Too Large", { status: HttpStatus.PAYLOAD_TOO_LARGE }));
383
+ if (err instanceof RequestBodyLengthMismatchError) return respond(text("Bad Request", { status: HttpStatus.BAD_REQUEST }));
384
+ throw err;
385
+ }
386
+ }
387
+ });
388
+ }
389
+ //#endregion
390
+ //#region src/router/middleware/start-time-ctx.ts
391
+ const startTimeCtx = () => (ctx) => {
392
+ ctx.startTime = Date.now();
393
+ };
394
+ //#endregion
395
+ //#region src/router/middleware/accept-ctx.ts
396
+ /**
397
+ * Attach parsed Accept header values to the request context.
398
+ *
399
+ * @returns Middleware that adds `accept` to the request context.
400
+ */
401
+ const acceptCtx = () => (ctx) => {
402
+ ctx.accept = parseAcceptHeader(ctx.request.headers.get("accept") ?? "*/*");
403
+ };
404
+ //#endregion
405
+ //#region src/router/lib/host.ts
406
+ function isLocalhost(hostname) {
407
+ const lower = hostname.toLowerCase();
408
+ return lower === "localhost" || lower === "127.0.0.1" || lower === "::1" || lower === "0.0.0.0";
409
+ }
410
+ //#endregion
411
+ //#region src/router/middleware/cors.ts
412
+ const headerOrigin = CommonHeaders.ORIGIN;
413
+ const headerVary = CommonHeaders.VARY;
414
+ const headerAccessControlRequestMethod = CommonHeaders.ACCESS_CONTROL_REQUEST_METHOD;
415
+ const headerAccessControlRequestHeaders = CommonHeaders.ACCESS_CONTROL_REQUEST_HEADERS;
416
+ const headerAllowOrigin = CommonHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
417
+ const headerAllowMethods = CommonHeaders.ACCESS_CONTROL_ALLOW_METHODS;
418
+ const headerAllowHeaders = CommonHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
419
+ const headerAllowCredentials = CommonHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
420
+ const headerExposeHeaders = CommonHeaders.ACCESS_CONTROL_EXPOSE_HEADERS;
421
+ const headerMaxAge = CommonHeaders.ACCESS_CONTROL_MAX_AGE;
422
+ const defaultAllowedMethods = [
423
+ HttpMethod.GET,
424
+ HttpMethod.HEAD,
425
+ HttpMethod.PUT,
426
+ HttpMethod.POST,
427
+ HttpMethod.DELETE,
428
+ HttpMethod.PATCH
429
+ ];
430
+ function normalizeHeaderValue(value) {
431
+ if (!value) return null;
432
+ const trimmed = value.trim();
433
+ return trimmed ? trimmed : null;
434
+ }
435
+ function normalizeOriginHeader(value) {
436
+ return normalizeHeaderValue(value);
437
+ }
438
+ function parseOrigin(originHeader) {
439
+ if (!originHeader || originHeader === "null") return null;
440
+ try {
441
+ return new URL(originHeader);
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+ function normalizeList(value) {
447
+ if (!value) return [];
448
+ return (Array.isArray(value) ? value : [value]).map((entry) => entry.trim()).filter(Boolean);
449
+ }
450
+ function normalizeMethods(value) {
451
+ return normalizeList(value).map((entry) => entry.toUpperCase());
452
+ }
453
+ function formatHeaderList(value) {
454
+ const list = normalizeList(value);
455
+ if (!list.length) return null;
456
+ return list.join(", ");
457
+ }
458
+ function formatMethodList(value) {
459
+ const list = normalizeMethods(value);
460
+ if (!list.length) return null;
461
+ return list.join(", ");
462
+ }
463
+ function normalizeAllowedOrigins(value) {
464
+ if (!value) return [];
465
+ const list = Array.isArray(value) ? value : [value];
466
+ const normalized = [];
467
+ for (const entry of list) {
468
+ if (entry instanceof RegExp) {
469
+ normalized.push({
470
+ kind: "regex",
471
+ value: entry
472
+ });
473
+ continue;
474
+ }
475
+ if (entry instanceof URL) {
476
+ normalized.push({
477
+ kind: "origin",
478
+ value: entry.origin
479
+ });
480
+ continue;
481
+ }
482
+ const trimmed = entry.trim();
483
+ if (!trimmed) continue;
484
+ if (trimmed === "null") {
485
+ normalized.push({ kind: "null" });
486
+ continue;
487
+ }
488
+ if (trimmed.includes("://")) try {
489
+ normalized.push({
490
+ kind: "origin",
491
+ value: new URL(trimmed).origin
492
+ });
493
+ } catch {
494
+ continue;
495
+ }
496
+ else normalized.push({
497
+ kind: "host",
498
+ value: trimmed.replace(/\/+$/, "")
499
+ });
500
+ }
501
+ return normalized;
502
+ }
503
+ function hasWildcardOrigin(value) {
504
+ if (!value) return false;
505
+ if (!Array.isArray(value)) return typeof value === "string" && value.trim() === "*";
506
+ return value.some((entry) => typeof entry === "string" && entry.trim() === "*");
507
+ }
508
+ function isOriginAllowed(originHeader, originUrl, allowlist, allowLocalhost) {
509
+ if (!originHeader) return false;
510
+ if (originHeader === "null") return allowlist.some((entry) => entry.kind === "null");
511
+ if (originUrl && allowLocalhost && isLocalhost(originUrl.hostname)) return true;
512
+ for (const entry of allowlist) {
513
+ if (entry.kind === "regex") {
514
+ if (entry.value.test(originHeader)) return true;
515
+ continue;
516
+ }
517
+ if (!originUrl) continue;
518
+ if (entry.kind === "origin") {
519
+ if (originUrl.origin === entry.value) return true;
520
+ continue;
521
+ }
522
+ if (entry.kind === "host") {
523
+ if (entry.value.includes(":")) {
524
+ if (originUrl.host === entry.value) return true;
525
+ } else if (originUrl.hostname === entry.value) return true;
526
+ }
527
+ }
528
+ return false;
529
+ }
530
+ function addVaryHeader(headers, value) {
531
+ const current = headers.get(headerVary);
532
+ if (!current) {
533
+ headers.set(headerVary, value);
534
+ return;
535
+ }
536
+ if (current.split(",").map((entry) => entry.trim().toLowerCase()).includes(value.toLowerCase())) return;
537
+ headers.set(headerVary, `${current}, ${value}`);
538
+ }
539
+ function applyCorsHeaders(headers, allowOrigin, allowCredentials, exposeHeaders, varyOrigin) {
540
+ headers.set(headerAllowOrigin, allowOrigin);
541
+ if (allowCredentials) headers.set(headerAllowCredentials, "true");
542
+ const expose = formatHeaderList(exposeHeaders);
543
+ if (expose) headers.set(headerExposeHeaders, expose);
544
+ if (varyOrigin) addVaryHeader(headers, "Origin");
545
+ }
546
+ function isAsyncGenerator(value) {
547
+ return !!value && typeof value[Symbol.asyncIterator] === "function";
548
+ }
549
+ function wrapGeneratorWithCors(generator, allowOrigin, allowCredentials, exposeHeaders, varyOrigin) {
550
+ const apply = (headers) => {
551
+ applyCorsHeaders(headers, allowOrigin, allowCredentials, exposeHeaders, varyOrigin);
552
+ };
553
+ async function* wrapped() {
554
+ let headersInjected = false;
555
+ while (true) {
556
+ const next = await generator.next();
557
+ if (next.done) {
558
+ if (!headersInjected) {
559
+ const headers$1 = new Headers();
560
+ apply(headers$1);
561
+ yield headers(headers$1);
562
+ }
563
+ return next.value;
564
+ }
565
+ const value = next.value;
566
+ if (isHeadersDirective(value)) {
567
+ const headers$2 = new Headers(value.headers);
568
+ apply(headers$2);
569
+ headersInjected = true;
570
+ yield headers(headers$2);
571
+ continue;
572
+ }
573
+ if (isHeadDirective(value)) {
574
+ const headers = new Headers(value.headers);
575
+ apply(headers);
576
+ headersInjected = true;
577
+ yield {
578
+ ...value,
579
+ headers
580
+ };
581
+ continue;
582
+ }
583
+ if (isStreamDirective(value)) {
584
+ const headers = new Headers(value.headers);
585
+ apply(headers);
586
+ headersInjected = true;
587
+ yield {
588
+ ...value,
589
+ headers
590
+ };
591
+ continue;
592
+ }
593
+ if (!headersInjected && isChunkDirective(value)) {
594
+ const headers$3 = new Headers();
595
+ apply(headers$3);
596
+ headersInjected = true;
597
+ yield headers(headers$3);
598
+ }
599
+ yield value;
600
+ }
601
+ }
602
+ return wrapped();
603
+ }
604
+ /**
605
+ * Attach CORS response headers and handle OPTIONS preflight requests.
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * router.use(cors({origin: '*'}))
610
+ * router.use(cors({origin: ['https://app.example.com'], credentials: true}))
611
+ * router.use(cors({origin: 'https://app.example.com', dev: true}))
612
+ * ```
613
+ *
614
+ * @param options - Configuration for origin, preflight, and header behavior.
615
+ * @returns Middleware that applies CORS headers to matching requests.
616
+ */
617
+ function cors(options) {
618
+ const allowCredentials = options.credentials ?? false;
619
+ const allowLocalhost = options.allowLocalhost ?? options.dev ?? false;
620
+ const originOption = options.origin;
621
+ const originResolver = typeof originOption === "function" ? originOption : void 0;
622
+ const originList = originResolver ? void 0 : originOption;
623
+ const allowlist = originList ? normalizeAllowedOrigins(originList) : [];
624
+ const hasWildcard = originList ? hasWildcardOrigin(originList) : false;
625
+ const preflightStatus = options.preflightStatus ?? HttpStatus.NO_CONTENT;
626
+ return defineMiddleware({
627
+ responses: { [preflightStatus]: {
628
+ schema: { type: "null" },
629
+ parse(value) {
630
+ if (value !== void 0) throw new TypeError("CORS preflight responses must not contain a body.");
631
+ }
632
+ } },
633
+ schemaAppliesTo: ({ method }) => Array.isArray(method) ? method.includes(HttpMethod.OPTIONS) : method === HttpMethod.OPTIONS,
634
+ async run(ctx, { next, forward, respond }) {
635
+ const originHeader = normalizeOriginHeader(ctx.request.headers.get(headerOrigin));
636
+ const originUrl = parseOrigin(originHeader);
637
+ let allowOrigin = null;
638
+ let varyOrigin = false;
639
+ if (originResolver) {
640
+ const resolved = await originResolver(originHeader, ctx);
641
+ if (resolved) {
642
+ const normalized = String(resolved).trim();
643
+ if (normalized === "*") {
644
+ if (allowCredentials && originHeader) {
645
+ allowOrigin = originUrl?.origin ?? originHeader;
646
+ varyOrigin = true;
647
+ } else if (!allowCredentials) allowOrigin = "*";
648
+ } else if (normalized) {
649
+ allowOrigin = normalized;
650
+ varyOrigin = Boolean(originHeader);
651
+ }
652
+ }
653
+ } else if (hasWildcard) {
654
+ if (allowCredentials && originHeader) {
655
+ allowOrigin = originUrl?.origin ?? originHeader;
656
+ varyOrigin = true;
657
+ } else if (!allowCredentials) allowOrigin = "*";
658
+ } else if (originHeader && isOriginAllowed(originHeader, originUrl, allowlist, allowLocalhost)) {
659
+ allowOrigin = originUrl?.origin ?? originHeader;
660
+ varyOrigin = true;
661
+ }
662
+ if (ctx.request.method.toUpperCase() === "OPTIONS" && ctx.request.headers.has(headerAccessControlRequestMethod)) {
663
+ if (!allowOrigin) return respond(empty(preflightStatus));
664
+ const headers = new Headers();
665
+ applyCorsHeaders(headers, allowOrigin, allowCredentials, void 0, varyOrigin);
666
+ const allowMethodsValue = formatMethodList(typeof options.allowMethods === "function" ? await options.allowMethods(originHeader, ctx) : options.allowMethods ?? defaultAllowedMethods);
667
+ if (allowMethodsValue) headers.set(headerAllowMethods, allowMethodsValue);
668
+ const requestHeaders = normalizeHeaderValue(ctx.request.headers.get(headerAccessControlRequestHeaders));
669
+ const allowHeadersValue = formatHeaderList(options.allowHeaders ?? requestHeaders ?? void 0);
670
+ if (allowHeadersValue) headers.set(headerAllowHeaders, allowHeadersValue);
671
+ if (options.maxAge != null) headers.set(headerMaxAge, String(options.maxAge));
672
+ return respond(empty(preflightStatus, { headers }));
673
+ }
674
+ const result = await next();
675
+ if (!allowOrigin) return forward(result);
676
+ if (result instanceof Response) {
677
+ applyCorsHeaders(result.headers, allowOrigin, allowCredentials, options.exposeHeaders, varyOrigin);
678
+ return forward(result);
679
+ }
680
+ if (isRoutekitResponse(result)) {
681
+ applyCorsHeaders(result.headers, allowOrigin, allowCredentials, options.exposeHeaders, varyOrigin);
682
+ return forward(result);
683
+ }
684
+ if (isAsyncGenerator(result)) return forward(wrapGeneratorWithCors(result, allowOrigin, allowCredentials, options.exposeHeaders, varyOrigin));
685
+ if (result != null && isResponseBodyInit(result)) {
686
+ const headers = new Headers();
687
+ applyCorsHeaders(headers, allowOrigin, allowCredentials, options.exposeHeaders, varyOrigin);
688
+ return forward(new Response(result, { headers }));
689
+ }
690
+ const headers = new Headers();
691
+ applyCorsHeaders(headers, allowOrigin, allowCredentials, options.exposeHeaders, varyOrigin);
692
+ return forward(response(result, { headers }));
693
+ }
694
+ });
695
+ }
696
+ //#endregion
697
+ //#region src/router/middleware/rate-limit.ts
698
+ const DEFAULT_MAX_ENTRIES = 1e5;
699
+ const ASN_OVERRIDES = {
700
+ 16509: "cloud",
701
+ 14618: "cloud",
702
+ 15169: "cloud",
703
+ 8075: "cloud",
704
+ 31898: "cloud",
705
+ 45102: "cloud",
706
+ 132203: "cloud",
707
+ 36351: "cloud",
708
+ 13335: "cdn",
709
+ 54113: "cdn",
710
+ 20940: "cdn",
711
+ 14061: "cloud",
712
+ 20473: "cloud",
713
+ 63949: "cloud",
714
+ 24940: "hosting",
715
+ 16276: "hosting",
716
+ 12876: "hosting",
717
+ 8560: "hosting",
718
+ 47583: "hosting",
719
+ 22612: "hosting"
720
+ };
721
+ const KEYWORDS = {
722
+ cdn: [
723
+ "cloudflare",
724
+ "fastly",
725
+ "akamai",
726
+ "cdn"
727
+ ],
728
+ cloud: [
729
+ "amazon",
730
+ "aws",
731
+ "google",
732
+ "gcp",
733
+ "microsoft",
734
+ "azure",
735
+ "oracle",
736
+ "alibaba",
737
+ "tencent",
738
+ "digitalocean",
739
+ "vultr",
740
+ "linode",
741
+ "ibm"
742
+ ],
743
+ hosting: [
744
+ "hosting",
745
+ "host",
746
+ "colo",
747
+ "datacenter",
748
+ "data center",
749
+ "ovh",
750
+ "scaleway",
751
+ "ionos",
752
+ "hostinger",
753
+ "namecheap"
754
+ ],
755
+ mobile: [
756
+ "mobile",
757
+ "wireless",
758
+ "cellular",
759
+ "lte",
760
+ "5g"
761
+ ],
762
+ residential: [
763
+ "telecom",
764
+ "broadband",
765
+ "cable",
766
+ "fiber"
767
+ ],
768
+ unknown: []
769
+ };
770
+ var InMemoryRateLimitStorage = class {
771
+ maxEntries;
772
+ store = /* @__PURE__ */ new Map();
773
+ constructor(maxEntries) {
774
+ this.maxEntries = maxEntries;
775
+ }
776
+ async readCounter(_ctx, key) {
777
+ const entry = this.store.get(key);
778
+ if (!entry) return null;
779
+ if (entry.expiresAtMs <= Date.now()) {
780
+ this.store.delete(key);
781
+ return null;
782
+ }
783
+ this.store.delete(key);
784
+ this.store.set(key, entry);
785
+ return entry.counter;
786
+ }
787
+ async writeCounter(_ctx, key, counter, ttlMs) {
788
+ const expiresAtMs = Date.now() + ttlMs;
789
+ if (this.store.has(key)) this.store.delete(key);
790
+ this.store.set(key, {
791
+ counter,
792
+ expiresAtMs
793
+ });
794
+ this.evictIfNeeded();
795
+ }
796
+ evictIfNeeded() {
797
+ while (this.store.size > this.maxEntries) {
798
+ const firstKey = this.store.keys().next().value;
799
+ if (!firstKey) return;
800
+ this.store.delete(firstKey);
801
+ }
802
+ }
803
+ };
804
+ function defaultGetIpAddress(ctx) {
805
+ const forwardedFor = ctx.request.headers.get("x-forwarded-for");
806
+ if (forwardedFor) {
807
+ const first = forwardedFor.split(",")[0]?.trim();
808
+ if (first) return Promise.resolve(cleanIpAddress(first));
809
+ }
810
+ const realIp = ctx.request.headers.get("x-real-ip");
811
+ if (realIp) return Promise.resolve(cleanIpAddress(realIp.trim()));
812
+ return Promise.resolve("unknown");
813
+ }
814
+ function cleanIpAddress(ip) {
815
+ let value = ip.trim();
816
+ if (!value) return "unknown";
817
+ if (value.startsWith("[")) {
818
+ const closing = value.indexOf("]");
819
+ if (closing !== -1) value = value.slice(1, closing);
820
+ }
821
+ const zoneIndex = value.indexOf("%");
822
+ if (zoneIndex !== -1) value = value.slice(0, zoneIndex);
823
+ if (value.includes(".")) {
824
+ const lastSegment = value.split(":").pop()?.trim();
825
+ if (lastSegment && parseIpv4Address(lastSegment)) return lastSegment;
826
+ if (value.split(":").length === 2) {
827
+ const segment = value.split(":")[0];
828
+ if (!segment) return "unknown";
829
+ value = segment;
830
+ }
831
+ }
832
+ if (parseIpv4Address(value) || parseIpv6Hextets(value)) return value;
833
+ return "unknown";
834
+ }
835
+ function normalizeQueryString(url) {
836
+ const entries = Array.from(url.searchParams.entries());
837
+ entries.sort((a, b) => {
838
+ const keyCompare = a[0].localeCompare(b[0]);
839
+ if (keyCompare !== 0) return keyCompare;
840
+ return a[1].localeCompare(b[1]);
841
+ });
842
+ const normalized = new URLSearchParams();
843
+ for (const [key, value] of entries) normalized.append(key, value);
844
+ return normalized.toString();
845
+ }
846
+ function getBucketMax(baseMax, baseWindowMs, bucket) {
847
+ return Math.floor(baseMax * (bucket.windowMs / baseWindowMs) * bucket.scale);
848
+ }
849
+ function getBucketResetAt(nowMs, windowMs) {
850
+ return Math.floor(nowMs / windowMs) * windowMs + windowMs;
851
+ }
852
+ async function applyFixedWindowLimit(ctx, storage, key, windowMs, max, nowMs, retentionMs) {
853
+ const stored = await storage.readCounter(ctx, key);
854
+ const resetAtMs = getBucketResetAt(nowMs, windowMs);
855
+ const counter = stored && stored.resetAtMs > nowMs ? stored : {
856
+ resetAtMs,
857
+ count: 0
858
+ };
859
+ const nextCount = counter.count + 1;
860
+ const updated = {
861
+ resetAtMs: counter.resetAtMs,
862
+ count: nextCount
863
+ };
864
+ const ttlEffective = Math.max(1, counter.resetAtMs - nowMs + 1e3) + Math.max(0, retentionMs);
865
+ await storage.writeCounter(ctx, key, updated, ttlEffective);
866
+ return {
867
+ allowed: nextCount <= max,
868
+ resetAtMs: counter.resetAtMs
869
+ };
870
+ }
871
+ function toURLPattern(pattern) {
872
+ const URLPatternCtor = ensureURLPattern();
873
+ if (pattern instanceof URLPatternCtor) return pattern;
874
+ if (Array.isArray(pattern)) return new URLPatternCtor(...pattern);
875
+ if (typeof pattern === "string") {
876
+ if (pattern.startsWith("http://") || pattern.startsWith("https://")) return new URLPatternCtor(pattern);
877
+ return new URLPatternCtor({ pathname: pattern });
878
+ }
879
+ return new URLPatternCtor(pattern);
880
+ }
881
+ function resolveEndpointLimit(method, url, matchers) {
882
+ const normalizedMethod = method.toUpperCase();
883
+ let minLimit = null;
884
+ for (const matcher of matchers) {
885
+ if (!matcher.pattern.test(url)) continue;
886
+ const limit = matcher.limit;
887
+ const methodLimit = typeof limit === "number" ? limit : limit[normalizedMethod];
888
+ if (methodLimit == null) continue;
889
+ minLimit = minLimit == null ? methodLimit : Math.min(minLimit, methodLimit);
890
+ }
891
+ return minLimit;
892
+ }
893
+ function parseIpv4Address(ip) {
894
+ const parts = ip.split(".");
895
+ if (parts.length !== 4) return null;
896
+ const bytes = [];
897
+ for (const part of parts) {
898
+ if (!part) return null;
899
+ const value = Number(part);
900
+ if (!Number.isInteger(value) || value < 0 || value > 255) return null;
901
+ bytes.push(value);
902
+ }
903
+ return bytes;
904
+ }
905
+ function parseIpv6Hextets(ip) {
906
+ let value = ip.toLowerCase();
907
+ const zoneIndex = value.indexOf("%");
908
+ if (zoneIndex !== -1) value = value.slice(0, zoneIndex);
909
+ const halves = value.split("::");
910
+ if (halves.length > 2) return null;
911
+ const left = halves[0] ? halves[0].split(":") : [];
912
+ const right = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
913
+ const leftParsed = parseIpv6Segments(left);
914
+ if (!leftParsed) return null;
915
+ const rightParsed = parseIpv6Segments(right);
916
+ if (!rightParsed) return null;
917
+ if (halves.length === 1) {
918
+ if (leftParsed.length !== 8) return null;
919
+ return leftParsed;
920
+ }
921
+ const missing = 8 - (leftParsed.length + rightParsed.length);
922
+ if (missing < 0) return null;
923
+ return [
924
+ ...leftParsed,
925
+ ...new Array(missing).fill(0),
926
+ ...rightParsed
927
+ ];
928
+ }
929
+ function parseIpv6Segments(parts) {
930
+ const hextets = [];
931
+ for (let index = 0; index < parts.length; index += 1) {
932
+ const part = parts[index];
933
+ if (!part) return null;
934
+ if (part.includes(".")) {
935
+ if (index !== parts.length - 1) return null;
936
+ const bytes = parseIpv4Address(part);
937
+ if (!bytes) return null;
938
+ if (bytes.length !== 4) return null;
939
+ const [b0, b1, b2, b3] = bytes;
940
+ if (b0 == null || b1 == null || b2 == null || b3 == null) return null;
941
+ hextets.push(b0 << 8 | b1, b2 << 8 | b3);
942
+ continue;
943
+ }
944
+ const value = Number.parseInt(part, 16);
945
+ if (!Number.isFinite(value) || value < 0 || value > 65535) return null;
946
+ hextets.push(value);
947
+ }
948
+ return hextets;
949
+ }
950
+ function deriveSubnet(ipAddress, ipv4Prefix = 24, ipv6Prefix = 64) {
951
+ const ipv4 = parseIpv4Address(ipAddress);
952
+ if (ipv4) {
953
+ if (ipv4.length !== 4) return {
954
+ key: "subnet:unknown",
955
+ version: "unknown"
956
+ };
957
+ const [o1, o2, o3] = ipv4;
958
+ if (o1 == null || o2 == null || o3 == null) return {
959
+ key: "subnet:unknown",
960
+ version: "unknown"
961
+ };
962
+ if (!Number.isInteger(ipv4Prefix) || ipv4Prefix < 0 || ipv4Prefix > 32) return {
963
+ key: "subnet:unknown",
964
+ version: "unknown"
965
+ };
966
+ if (ipv4Prefix !== 24) {
967
+ const network = (o1 << 24 | o2 << 16 | o3 << 8 | (ipv4[3] ?? 0)) >>> 0 & (ipv4Prefix === 0 ? 0 : 4294967295 << 32 - ipv4Prefix >>> 0);
968
+ return {
969
+ key: `subnet:ip4:${network >>> 24 & 255}.${network >>> 16 & 255}.${network >>> 8 & 255}.${network & 255}/${ipv4Prefix}`,
970
+ version: "ipv4"
971
+ };
972
+ }
973
+ return {
974
+ key: `subnet:ip24:${o1}.${o2}.${o3}.0/24`,
975
+ version: "ipv4"
976
+ };
977
+ }
978
+ const ipv6 = parseIpv6Hextets(ipAddress);
979
+ if (ipv6 && ipv6.length === 8) {
980
+ const h1 = ipv6[0];
981
+ const h2 = ipv6[1];
982
+ const h3 = ipv6[2];
983
+ const h4 = ipv6[3];
984
+ if (!Number.isInteger(ipv6Prefix) || ipv6Prefix < 0 || ipv6Prefix > 128) return {
985
+ key: "subnet:unknown",
986
+ version: "unknown"
987
+ };
988
+ if (ipv6Prefix !== 64) return {
989
+ key: `subnet:ip6:${maskIpv6(ipv6, ipv6Prefix).map((value) => value.toString(16)).join(":")}/${ipv6Prefix}`,
990
+ version: "ipv6"
991
+ };
992
+ return {
993
+ key: `subnet:ip64:${h1.toString(16)}:${h2.toString(16)}:${h3.toString(16)}:${h4.toString(16)}::/64`,
994
+ version: "ipv6"
995
+ };
996
+ }
997
+ return {
998
+ key: "subnet:unknown",
999
+ version: "unknown"
1000
+ };
1001
+ }
1002
+ function maskIpv6(hextets, prefix) {
1003
+ const masked = [];
1004
+ let remaining = prefix;
1005
+ for (const hextet of hextets) {
1006
+ if (remaining >= 16) {
1007
+ masked.push(hextet);
1008
+ remaining -= 16;
1009
+ continue;
1010
+ }
1011
+ if (remaining <= 0) {
1012
+ masked.push(0);
1013
+ continue;
1014
+ }
1015
+ const mask = (65535 << 16 - remaining & 65535) >>> 0;
1016
+ masked.push(hextet & mask);
1017
+ remaining = 0;
1018
+ }
1019
+ return masked;
1020
+ }
1021
+ function defaultAsnToClass(asn, organization) {
1022
+ const override = ASN_OVERRIDES[asn];
1023
+ if (override) return override;
1024
+ const normalizedOrg = organization.toLowerCase();
1025
+ const entries = Object.entries(KEYWORDS);
1026
+ for (const [asnClass, keywords] of entries) {
1027
+ if (asnClass === "unknown") continue;
1028
+ if (keywords.some((keyword) => normalizedOrg.includes(keyword))) return asnClass;
1029
+ }
1030
+ return "unknown";
1031
+ }
1032
+ function normalizeAsnClass(asnClass) {
1033
+ if (!asnClass) return "unknown";
1034
+ return asnClass;
1035
+ }
1036
+ function normalizeCountryCode(code) {
1037
+ if (!code) return null;
1038
+ const trimmed = code.trim();
1039
+ if (!trimmed) return null;
1040
+ return trimmed.toUpperCase();
1041
+ }
1042
+ async function loadMaxmindModule() {
1043
+ try {
1044
+ return await import("maxmind");
1045
+ } catch (err) {
1046
+ throw new Error("maxmind is required for ASN or country lookups; install it as a peer dependency", { cause: err });
1047
+ }
1048
+ }
1049
+ function createGeoResolvers(options) {
1050
+ let maxmindModulePromise = null;
1051
+ let asnReaderPromise = null;
1052
+ let countryReaderPromise = null;
1053
+ const loadMaxmind = () => {
1054
+ if (!maxmindModulePromise) maxmindModulePromise = loadMaxmindModule();
1055
+ return maxmindModulePromise;
1056
+ };
1057
+ const loadAsnReader = async () => {
1058
+ if (!options.maxmindAsnDatabase) return null;
1059
+ if (!asnReaderPromise) asnReaderPromise = loadMaxmind().then((module) => module.open(options.maxmindAsnDatabase));
1060
+ return asnReaderPromise;
1061
+ };
1062
+ const loadCountryReader = async () => {
1063
+ if (!options.maxmindCountryDatabase) return null;
1064
+ if (!countryReaderPromise) countryReaderPromise = loadMaxmind().then((module) => module.open(options.maxmindCountryDatabase));
1065
+ return countryReaderPromise;
1066
+ };
1067
+ return {
1068
+ getAsn: options.getAsn ?? (async (_ctx, input) => {
1069
+ const reader = await loadAsnReader();
1070
+ if (!reader) return null;
1071
+ const record = reader.get(input.ipAddress);
1072
+ const asn = record?.autonomous_system_number ?? record?.autonomousSystemNumber;
1073
+ const organization = record?.autonomous_system_organization ?? record?.autonomousSystemOrganization ?? record?.organization;
1074
+ if (typeof asn !== "number" || !organization) return null;
1075
+ return {
1076
+ asn,
1077
+ organization: String(organization)
1078
+ };
1079
+ }),
1080
+ getCountry: options.getCountryCode ?? (async (_ctx, input) => {
1081
+ const reader = await loadCountryReader();
1082
+ if (!reader) return null;
1083
+ const record = reader.get(input.ipAddress);
1084
+ const code = record?.country?.iso_code ?? record?.country?.isoCode ?? record?.registered_country?.iso_code ?? record?.registeredCountry?.isoCode ?? record?.represented_country?.iso_code ?? record?.representedCountry?.isoCode;
1085
+ return normalizeCountryCode(typeof code === "string" ? code : null);
1086
+ })
1087
+ };
1088
+ }
1089
+ function formatRetryAfterSeconds(resetAtMs, nowMs) {
1090
+ const seconds = Math.max(1, Math.ceil((resetAtMs - nowMs) / 1e3));
1091
+ return String(seconds);
1092
+ }
1093
+ function buildTooManyRequests(addRetryAfterHeader, resetAtMs, nowMs) {
1094
+ const response = text("Too Many Requests", { status: HttpStatus.TOO_MANY_REQUESTS });
1095
+ if (addRetryAfterHeader) response.headers.set("Retry-After", formatRetryAfterSeconds(resetAtMs, nowMs));
1096
+ return response;
1097
+ }
1098
+ /**
1099
+ * Enforce per-identity, subnet, ASN, country, and endpoint rate limits using fixed-window buckets.
1100
+ *
1101
+ * @example
1102
+ * ```ts
1103
+ * router.use(rateLimit({
1104
+ * getUserId: async ({user}) => user?.id,
1105
+ * getGlobalPeakConcurrentUsers: async () => 5000,
1106
+ * baseWindowMs: 60_000,
1107
+ * baseMaxRequestsPerBaseWindow: 120,
1108
+ * anonymousIpMultiplier: 0.5,
1109
+ * addRetryAfterHeader: true,
1110
+ * buckets: [{windowMs: 60_000, scale: 1}],
1111
+ * endpointLimits: [{pattern: '/login', limit: {POST: 10}}],
1112
+ * includeQueryInEndpointKey: false,
1113
+ * scales: {subnet: {ipv4: 2, ipv6: 1}},
1114
+ * }))
1115
+ * ```
1116
+ *
1117
+ * @param options - Configuration for identity sources, buckets, scaling, and storage.
1118
+ * @returns Middleware that enforces rate limits and returns 429 responses when exceeded.
1119
+ */
1120
+ function rateLimit(options) {
1121
+ if (!options.buckets.length) throw new Error("rateLimit requires at least one bucket");
1122
+ const storage = options.storage ?? new InMemoryRateLimitStorage(options.inMemory?.maxEntries ?? DEFAULT_MAX_ENTRIES);
1123
+ const getIpAddress = options.getIpAddress ?? defaultGetIpAddress;
1124
+ const normalizeQuery = options.normalizeQuery ?? normalizeQueryString;
1125
+ const endpointMatchers = options.endpointLimits.map((limit) => ({
1126
+ pattern: toURLPattern(limit.pattern),
1127
+ limit: limit.limit
1128
+ }));
1129
+ const asnToClass = options.asnToClass ?? defaultAsnToClass;
1130
+ const geoResolvers = createGeoResolvers(options);
1131
+ const asnLimitEnabled = Boolean(options.scales.asnClass && (options.getAsn || options.maxmindAsnDatabase));
1132
+ const countryLimitEnabled = Boolean(options.scales.country && (options.getCountryCode || options.maxmindCountryDatabase));
1133
+ const subnetAsnClassEnabled = Boolean(options.scales.subnet.byAsnClass && (options.getAsn || options.maxmindAsnDatabase));
1134
+ const retentionMs = options.storage ? 0 : options.inMemory?.ttlMs ?? 1e3;
1135
+ return defineMiddleware({
1136
+ responses: { [HttpStatus.TOO_MANY_REQUESTS]: {
1137
+ schema: { type: "string" },
1138
+ parse(value) {
1139
+ if (typeof value !== "string") throw new TypeError("Rate limit responses must contain a string body.");
1140
+ return value;
1141
+ }
1142
+ } },
1143
+ async run(ctx, { next, forward, respond }) {
1144
+ const reject = (addRetryAfterHeader, resetAtMs, nowMs) => respond(buildTooManyRequests(addRetryAfterHeader, resetAtMs, nowMs));
1145
+ const nowMs = ctx.startTime ?? Date.now();
1146
+ const url = ctx.request.url;
1147
+ const method = ctx.request.method.toUpperCase();
1148
+ const userId = await options.getUserId(ctx);
1149
+ const ipAddress = cleanIpAddress(await getIpAddress(ctx));
1150
+ const identityKey = userId ? `identity:user:${userId}` : `identity:ip:${ipAddress}`;
1151
+ const identityMultiplier = userId ? 1 : options.anonymousIpMultiplier;
1152
+ const subnet = deriveSubnet(ipAddress, options.scales.subnet.ipv4Prefix ?? 24, options.scales.subnet.ipv6Prefix ?? 64);
1153
+ const subnetScaleBase = subnet.version === "ipv6" ? options.scales.subnet.ipv6 : subnet.version === "ipv4" ? options.scales.subnet.ipv4 : Math.min(options.scales.subnet.ipv4, options.scales.subnet.ipv6);
1154
+ let asnRecord = null;
1155
+ let asnClass = "unknown";
1156
+ if (asnLimitEnabled || subnetAsnClassEnabled) {
1157
+ asnRecord = await geoResolvers.getAsn(ctx, {
1158
+ userId,
1159
+ ipAddress
1160
+ });
1161
+ asnClass = normalizeAsnClass(asnRecord ? asnToClass(asnRecord.asn, asnRecord.organization) : "unknown");
1162
+ }
1163
+ const subnetMultiplier = subnetScaleBase * (options.scales.subnet.byAsnClass?.[asnClass] ?? 1);
1164
+ let countryCode = null;
1165
+ if (countryLimitEnabled) countryCode = normalizeCountryCode(await geoResolvers.getCountry(ctx, {
1166
+ userId,
1167
+ ipAddress
1168
+ }));
1169
+ const endpointBaseLimit = endpointMatchers.length ? resolveEndpointLimit(method, url, endpointMatchers) : null;
1170
+ const endpointKeyBase = buildEndpointKeyBase(url, method, options.includeQueryInEndpointKey, normalizeQuery);
1171
+ const endpointIdentityKey = endpointKeyBase ? `endpoint:${endpointKeyBase}:${identityKey}` : null;
1172
+ const endpointSubnetKey = endpointKeyBase ? `endpoint:${endpointKeyBase}:${subnet.key}` : null;
1173
+ const globalPeak = asnLimitEnabled || countryLimitEnabled ? await options.getGlobalPeakConcurrentUsers(ctx) : 1;
1174
+ for (const bucket of options.buckets) {
1175
+ const bucketMax = getBucketMax(options.baseMaxRequestsPerBaseWindow, options.baseWindowMs, bucket);
1176
+ const bucketSuffix = `:w${bucket.windowMs}`;
1177
+ const identityMax = Math.floor(bucketMax * identityMultiplier);
1178
+ const identityResult = await applyFixedWindowLimit(ctx, storage, `${identityKey}${bucketSuffix}`, bucket.windowMs, identityMax, nowMs, retentionMs);
1179
+ if (!identityResult.allowed) return reject(options.addRetryAfterHeader, identityResult.resetAtMs, nowMs);
1180
+ const subnetMax = Math.floor(bucketMax * subnetMultiplier);
1181
+ const subnetResult = await applyFixedWindowLimit(ctx, storage, `${subnet.key}${bucketSuffix}`, bucket.windowMs, subnetMax, nowMs, retentionMs);
1182
+ if (!subnetResult.allowed) return reject(options.addRetryAfterHeader, subnetResult.resetAtMs, nowMs);
1183
+ if (asnLimitEnabled && options.scales.asnClass) {
1184
+ const asnScale = options.scales.asnClass[asnClass] ?? options.scales.asnClass.unknown;
1185
+ const asnMax = Math.floor(bucketMax * asnScale * globalPeak);
1186
+ const asnResult = await applyFixedWindowLimit(ctx, storage, `${asnRecord ? `asn:${asnRecord.asn}` : "asn:unknown"}${bucketSuffix}`, bucket.windowMs, asnMax, nowMs, retentionMs);
1187
+ if (!asnResult.allowed) return reject(options.addRetryAfterHeader, asnResult.resetAtMs, nowMs);
1188
+ }
1189
+ if (countryLimitEnabled && options.scales.country) {
1190
+ const countryScale = countryCode ? options.scales.country[countryCode] ?? options.scales.country.other : options.scales.country.unknown;
1191
+ const countryMax = Math.floor(bucketMax * countryScale * globalPeak);
1192
+ const countryResult = await applyFixedWindowLimit(ctx, storage, `${countryCode ? `country:${countryCode}` : "country:unknown"}${bucketSuffix}`, bucket.windowMs, countryMax, nowMs, retentionMs);
1193
+ if (!countryResult.allowed) return reject(options.addRetryAfterHeader, countryResult.resetAtMs, nowMs);
1194
+ }
1195
+ if (endpointBaseLimit != null && endpointKeyBase && endpointIdentityKey && endpointSubnetKey) {
1196
+ const endpointBucketMax = getBucketMax(endpointBaseLimit, options.baseWindowMs, bucket);
1197
+ const endpointIdentityMax = Math.floor(endpointBucketMax * identityMultiplier);
1198
+ const endpointSubnetMax = Math.floor(endpointBucketMax * subnetMultiplier);
1199
+ const endpointIdentityResult = await applyFixedWindowLimit(ctx, storage, `${endpointIdentityKey}${bucketSuffix}`, bucket.windowMs, endpointIdentityMax, nowMs, retentionMs);
1200
+ if (!endpointIdentityResult.allowed) return reject(options.addRetryAfterHeader, endpointIdentityResult.resetAtMs, nowMs);
1201
+ const endpointSubnetResult = await applyFixedWindowLimit(ctx, storage, `${endpointSubnetKey}${bucketSuffix}`, bucket.windowMs, endpointSubnetMax, nowMs, retentionMs);
1202
+ if (!endpointSubnetResult.allowed) return reject(options.addRetryAfterHeader, endpointSubnetResult.resetAtMs, nowMs);
1203
+ }
1204
+ }
1205
+ return forward(await next());
1206
+ }
1207
+ });
1208
+ }
1209
+ function buildEndpointKeyBase(url, method, includeQuery, normalizeQuery) {
1210
+ const pathname = url.pathname;
1211
+ const normalizedMethod = method.toUpperCase();
1212
+ if (!includeQuery) return `route:${normalizedMethod}:${pathname}`;
1213
+ const query = normalizeQuery(url);
1214
+ if (!query) return `routeq:${normalizedMethod}:${pathname}`;
1215
+ return `routeq:${normalizedMethod}:${pathname}?${query}`;
1216
+ }
1217
+ function ensureURLPattern() {
1218
+ if (typeof URLPattern === "undefined") throw new Error("URLPattern is not available in this runtime");
1219
+ return URLPattern;
1220
+ }
1221
+ //#endregion
1222
+ export { acceptCtx, bodyLimit, cors, defineMiddleware, formatRoutekitTerminalLogRecord, rateLimit, requestIdCtx, requestLogger, startTimeCtx, transformRoutekitJsonLogRecord };