@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,1025 @@
1
+ import { a as response } from "./core-DbmQauwS.mjs";
2
+ import { CommonContentTypes, CommonHeaders, HttpStatus, StatusText } from "@mpen/http";
3
+ //#region src/router/lib/charset.ts
4
+ /**
5
+ * Normalize a charset label to a lowercase "Preferred MIME Name" where possible.
6
+ *
7
+ * - Charset comparison is case-insensitive (IANA).
8
+ * - Uses Preferred MIME Names when mapped.
9
+ * - Unknown charsets return a cleaned lowercase form.
10
+ */
11
+ function normalizeCharsetName(input) {
12
+ if (!input?.length) return "";
13
+ const raw = input.trim();
14
+ if (raw.length === 0) return "";
15
+ const keyA = normalizeKey(raw);
16
+ const hitA = ALIAS_TO_PREFERRED_LOWER.get(keyA);
17
+ if (hitA) return hitA;
18
+ const keyB = stripKey(keyA);
19
+ const hitB = STRIPPED_ALIAS_TO_PREFERRED_LOWER.get(keyB);
20
+ if (hitB) return hitB;
21
+ return keyA;
22
+ }
23
+ function normalizeKey(s) {
24
+ return s.trim().toLowerCase().replace(/\s+/g, "").replace(/_/g, "-");
25
+ }
26
+ function stripKey(s) {
27
+ return s.replace(/[^a-z0-9]+/g, "");
28
+ }
29
+ /**
30
+ * Minimal, practical alias set.
31
+ * Extend this map as needed (ideally generated from IANA CSV for full coverage). :contentReference[oaicite:5]{index=5}
32
+ */
33
+ const ALIAS_TO_PREFERRED_LOWER = new Map([
34
+ ["utf-8", "utf-8"],
35
+ ["utf8", "utf-8"],
36
+ ["unicode-1-1-utf-8", "utf-8"],
37
+ ["utf-16", "utf-16"],
38
+ ["utf16", "utf-16"],
39
+ ["utf-16le", "utf-16le"],
40
+ ["utf-16be", "utf-16be"],
41
+ ["us-ascii", "us-ascii"],
42
+ ["ascii", "us-ascii"],
43
+ ["ansi_x3.4-1968", "us-ascii"],
44
+ ["ansi_x3.4-1986", "us-ascii"],
45
+ ["iso646-us", "us-ascii"],
46
+ ["cp367", "us-ascii"],
47
+ ["ibm367", "us-ascii"],
48
+ ["iso-8859-1", "iso-8859-1"],
49
+ ["iso_8859-1:1987", "iso-8859-1"],
50
+ ["iso_8859-1", "iso-8859-1"],
51
+ ["latin1", "iso-8859-1"],
52
+ ["l1", "iso-8859-1"],
53
+ ["cp819", "iso-8859-1"],
54
+ ["ibm819", "iso-8859-1"],
55
+ ["iso-8859-2", "iso-8859-2"],
56
+ ["latin2", "iso-8859-2"],
57
+ ["l2", "iso-8859-2"],
58
+ ["iso-8859-3", "iso-8859-3"],
59
+ ["latin3", "iso-8859-3"],
60
+ ["l3", "iso-8859-3"],
61
+ ["iso-8859-4", "iso-8859-4"],
62
+ ["latin4", "iso-8859-4"],
63
+ ["l4", "iso-8859-4"],
64
+ ["iso-8859-5", "iso-8859-5"],
65
+ ["cyrillic", "iso-8859-5"],
66
+ ["iso-8859-6", "iso-8859-6"],
67
+ ["arabic", "iso-8859-6"],
68
+ ["iso-8859-7", "iso-8859-7"],
69
+ ["greek", "iso-8859-7"],
70
+ ["greek8", "iso-8859-7"],
71
+ ["iso-8859-8", "iso-8859-8"],
72
+ ["hebrew", "iso-8859-8"],
73
+ ["iso-8859-9", "iso-8859-9"],
74
+ ["latin5", "iso-8859-9"],
75
+ ["l5", "iso-8859-9"],
76
+ ["windows-1252", "windows-1252"],
77
+ ["cp1252", "windows-1252"],
78
+ ["windows-1251", "windows-1251"],
79
+ ["cp1251", "windows-1251"],
80
+ ["shift_jis", "shift_jis"],
81
+ ["shift-jis", "shift_jis"],
82
+ ["sjis", "shift_jis"],
83
+ ["ms_kanji", "shift_jis"],
84
+ ["euc-jp", "euc-jp"],
85
+ ["eucjp", "euc-jp"],
86
+ ["euc-kr", "euc-kr"],
87
+ ["euckr", "euc-kr"],
88
+ ["iso-2022-jp", "iso-2022-jp"],
89
+ ["iso-2022-kr", "iso-2022-kr"],
90
+ ["big5", "big5"],
91
+ ["gbk", "gbk"],
92
+ ["gb18030", "gb18030"]
93
+ ]);
94
+ const STRIPPED_ALIAS_TO_PREFERRED_LOWER = new Map(Array.from(ALIAS_TO_PREFERRED_LOWER.entries(), ([k, v]) => [stripKey(k), v]));
95
+ //#endregion
96
+ //#region src/router/lib/media-type.ts
97
+ const tokenPattern = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
98
+ const rangeTokenPattern = /^(?:\*|[!#$%&'*+.^_`|~0-9a-z-]+)$/i;
99
+ function normalizeToken(value) {
100
+ return value.trim();
101
+ }
102
+ function normalizeType(value) {
103
+ return normalizeToken(value).toLowerCase();
104
+ }
105
+ function isValidType(value, allowRange) {
106
+ const [type, subtype, extra] = value.split("/");
107
+ if (!type || !subtype || extra !== void 0) return false;
108
+ if (!allowRange && (type.includes("*") || subtype.includes("*"))) return false;
109
+ const pattern = allowRange ? rangeTokenPattern : tokenPattern;
110
+ if (!pattern.test(type) || !pattern.test(subtype)) return false;
111
+ if (type === "*" && subtype !== "*") return false;
112
+ return true;
113
+ }
114
+ function splitParameters(value) {
115
+ const parts = [];
116
+ let current = "";
117
+ let inQuote = false;
118
+ let escaped = false;
119
+ for (const char of value) {
120
+ if (escaped) {
121
+ current += char;
122
+ escaped = false;
123
+ continue;
124
+ }
125
+ if (char === "\\" && inQuote) {
126
+ escaped = true;
127
+ current += char;
128
+ continue;
129
+ }
130
+ if (char === "\"") {
131
+ inQuote = !inQuote;
132
+ current += char;
133
+ continue;
134
+ }
135
+ if (char === ";" && !inQuote) {
136
+ parts.push(current);
137
+ current = "";
138
+ continue;
139
+ }
140
+ current += char;
141
+ }
142
+ parts.push(current);
143
+ return parts;
144
+ }
145
+ function unquote(value) {
146
+ const trimmed = value.trim();
147
+ if (!trimmed.startsWith("\"") || !trimmed.endsWith("\"")) return trimmed;
148
+ return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
149
+ }
150
+ function parseParameterizedMediaType(value, options) {
151
+ const [rawType = "", ...rawParams] = splitParameters(value);
152
+ const type = normalizeType(rawType);
153
+ if (!type || !isValidType(type, options.allowRange)) return null;
154
+ const parameters = {};
155
+ const result = { type };
156
+ for (const param of rawParams) {
157
+ const separatorIndex = param.indexOf("=");
158
+ if (separatorIndex <= 0) continue;
159
+ const key = param.slice(0, separatorIndex).trim().toLowerCase();
160
+ if (!key || !tokenPattern.test(key)) continue;
161
+ const paramValue = unquote(param.slice(separatorIndex + 1));
162
+ parameters[key] = paramValue;
163
+ if (key === "charset") result.charset = normalizeCharsetName(paramValue);
164
+ if (key === "boundary") result.boundary = paramValue;
165
+ }
166
+ const extraParameters = Object.fromEntries(Object.entries(parameters).filter(([key]) => key !== "charset" && key !== "boundary"));
167
+ if (Object.keys(extraParameters).length > 0) result.parameters = extraParameters;
168
+ return result;
169
+ }
170
+ function parseMediaRange(value) {
171
+ return parseParameterizedMediaType(value, { allowRange: true });
172
+ }
173
+ function normalizeQuality(value) {
174
+ if (value === void 0) return 1;
175
+ const q = typeof value === "number" ? value : Number.parseFloat(value);
176
+ return Number.isFinite(q) && q >= 0 && q <= 1 ? q : 1;
177
+ }
178
+ function formatContentType(value) {
179
+ const params = [];
180
+ if (value.charset) params.push(`charset=${value.charset}`);
181
+ if (value.boundary) params.push(`boundary=${value.boundary}`);
182
+ for (const [key, paramValue] of Object.entries(value.parameters ?? {})) {
183
+ if (key === "q") continue;
184
+ params.push(`${key}=${paramValue}`);
185
+ }
186
+ return [value.type, ...params].join("; ");
187
+ }
188
+ /**
189
+ * Normalize a concrete media type.
190
+ *
191
+ * @param value - Media type to normalize.
192
+ * @returns Normalized media type.
193
+ */
194
+ function normalizeMediaType(value) {
195
+ const type = normalizeType(value.type);
196
+ const charset = value.charset ? normalizeCharsetName(value.charset) : void 0;
197
+ const boundary = value.boundary ? normalizeToken(value.boundary) : void 0;
198
+ const parameters = value.parameters ? Object.fromEntries(Object.entries(value.parameters).map(([key, paramValue]) => [key.trim().toLowerCase(), paramValue.trim()])) : void 0;
199
+ const result = { type };
200
+ if (charset) result.charset = charset;
201
+ if (boundary) result.boundary = boundary;
202
+ if (parameters && Object.keys(parameters).length > 0) result.parameters = parameters;
203
+ return result;
204
+ }
205
+ /**
206
+ * Parse a single HTTP `Content-Type` header value.
207
+ *
208
+ * @param value - Header value to parse.
209
+ * @returns Parsed content type, or `null` when the value is invalid or missing.
210
+ */
211
+ function parseContentType(value) {
212
+ if (!value) return null;
213
+ return parseParameterizedMediaType(value, { allowRange: false });
214
+ }
215
+ /**
216
+ * Parse a concrete media type value.
217
+ *
218
+ * @param value - Media type value to parse.
219
+ * @returns Parsed media type, or `null` when the value is invalid.
220
+ */
221
+ function parseMediaType(value) {
222
+ return parseContentType(value);
223
+ }
224
+ /**
225
+ * Return the preference weight from an Accept-style media range.
226
+ *
227
+ * @param value - Media range value to inspect.
228
+ * @returns The parsed `q` value, or `1` when none is provided.
229
+ */
230
+ function mediaRangeQuality(value) {
231
+ if (typeof value !== "string") return normalizeQuality("q" in value ? value.q : value.parameters?.q);
232
+ return normalizeQuality(parseMediaRange(value)?.parameters?.q);
233
+ }
234
+ /**
235
+ * Format an Accept-style media range as a concrete `Content-Type`.
236
+ *
237
+ * @param value - Media range value to format.
238
+ * @returns The media type without any `q` preference parameter, or `null` when invalid.
239
+ */
240
+ function mediaRangeToContentType(value) {
241
+ const parsed = typeof value === "string" ? parseMediaRange(value) : normalizeMediaType(value);
242
+ if (!parsed) return null;
243
+ return formatContentType(parsed);
244
+ }
245
+ /**
246
+ * Parse an Accept header into quality-sorted media ranges.
247
+ *
248
+ * @param value - Accept header value to parse.
249
+ * @returns Media ranges sorted by descending `q` values, preserving original order for ties.
250
+ */
251
+ function parseAcceptHeader(value) {
252
+ const entries = [];
253
+ for (const [index, entry] of (value ?? "").split(",").entries()) {
254
+ const parsed = parseMediaRange(entry.trim());
255
+ if (!parsed) continue;
256
+ const qParameter = parsed.parameters?.q;
257
+ const parsedQ = qParameter === void 0 ? 1 : Number.parseFloat(qParameter);
258
+ const q = Number.isFinite(parsedQ) && parsedQ >= 0 && parsedQ <= 1 ? parsedQ : 1;
259
+ const parameters = { ...parsed.parameters ?? {} };
260
+ delete parameters.q;
261
+ const media = {
262
+ ...parsed,
263
+ q
264
+ };
265
+ delete media.parameters;
266
+ if (Object.keys(parameters).length > 0) media.parameters = parameters;
267
+ entries.push({
268
+ media,
269
+ index
270
+ });
271
+ }
272
+ entries.sort((a, b) => {
273
+ if (b.media.q !== a.media.q) return b.media.q - a.media.q;
274
+ return a.index - b.index;
275
+ });
276
+ return entries.map((entry) => {
277
+ const { parameters, ...media } = entry.media;
278
+ return parameters && Object.keys(parameters).length > 0 ? {
279
+ ...media,
280
+ parameters
281
+ } : media;
282
+ });
283
+ }
284
+ /**
285
+ * Test whether a media type is a JSON document type.
286
+ *
287
+ * @param value - Content type or header value to inspect.
288
+ * @returns Whether the media type is `application/json` or an `application/*+json` type.
289
+ */
290
+ function isJsonContentType(value) {
291
+ const contentType = typeof value === "string" ? parseContentType(value) : value;
292
+ if (!contentType) return false;
293
+ const [type, subtype] = contentType.type.split("/", 2);
294
+ return type === "application" && (subtype === "json" || subtype.endsWith("+json"));
295
+ }
296
+ /**
297
+ * Test whether an accepted concrete media type matches an incoming content type.
298
+ *
299
+ * @param accept - Media type accepted by a route.
300
+ * @param contentType - Incoming request content type.
301
+ * @returns Whether the accepted media type matches the content type.
302
+ */
303
+ function mediaTypeMatches(accept, contentType) {
304
+ const normalizedAccept = normalizeMediaType(accept);
305
+ const normalizedContent = normalizeMediaType(contentType);
306
+ if (normalizedAccept.type !== normalizedContent.type) return false;
307
+ if (normalizedAccept.charset && normalizedContent.charset) {
308
+ if (normalizeCharsetName(normalizedAccept.charset) !== normalizeCharsetName(normalizedContent.charset)) return false;
309
+ }
310
+ if (normalizedAccept.boundary && normalizedContent.boundary) {
311
+ if (normalizedAccept.boundary !== normalizedContent.boundary) return false;
312
+ }
313
+ return true;
314
+ }
315
+ /**
316
+ * Test whether an HTTP media range accepts a concrete media type.
317
+ *
318
+ * @param range - Accept-style media range.
319
+ * @param mediaType - Concrete media type to test.
320
+ * @returns Whether the range accepts the media type.
321
+ */
322
+ function mediaRangeAccepts(range, mediaType) {
323
+ const parsedRange = typeof range === "string" ? parseMediaRange(range) : normalizeMediaType(range);
324
+ const produced = typeof mediaType === "string" ? parseContentType(mediaType) : mediaType;
325
+ if (!parsedRange || !produced) return false;
326
+ const normalizedRange = parsedRange.type.toLowerCase();
327
+ const normalizedProduced = produced.type.toLowerCase();
328
+ if (normalizedRange === "*/*") return true;
329
+ if (normalizedRange === normalizedProduced) return true;
330
+ const [rangeType, rangeSubtype] = normalizedRange.split("/", 2);
331
+ const [producedType, producedSubtype] = normalizedProduced.split("/", 2);
332
+ if (!rangeType || !rangeSubtype || !producedType || !producedSubtype) return false;
333
+ if (rangeSubtype === "*" && rangeType === producedType) return true;
334
+ if (rangeSubtype.startsWith("*+")) return rangeType === producedType && producedSubtype.endsWith(rangeSubtype.slice(1));
335
+ return false;
336
+ }
337
+ //#endregion
338
+ //#region src/router/lib/schema-merge.ts
339
+ function unionJsonSchemas(left, right) {
340
+ return { anyOf: [left, right] };
341
+ }
342
+ /**
343
+ * Merge route schema contributions from routes and middleware.
344
+ *
345
+ * Response declarations at the same status are alternatives. Request declarations describe
346
+ * parsed context fields and therefore may only be declared once within an executed chain.
347
+ *
348
+ * @param schemas - Schema contributions in middleware execution order followed by the route.
349
+ * @returns The combined schema contribution, or `undefined` if none were supplied.
350
+ */
351
+ function mergeRouteSchemas(...schemas) {
352
+ const request = {};
353
+ const responseBody = {};
354
+ let hasRequest = false;
355
+ let hasResponse = false;
356
+ for (const schema of schemas) {
357
+ if (!schema) continue;
358
+ if (schema.request) for (const component of [
359
+ "query",
360
+ "path",
361
+ "body"
362
+ ]) {
363
+ const declaration = schema.request[component];
364
+ if (declaration === void 0) continue;
365
+ if (request[component] !== void 0) throw new Error(`Multiple middleware or route schemas declare request.${component}.`);
366
+ request[component] = declaration;
367
+ hasRequest = true;
368
+ }
369
+ for (const [status, declaration] of Object.entries(schema.response?.body ?? {})) {
370
+ if (declaration === void 0) continue;
371
+ const normalizedStatus = status === "default" ? status : Number(status);
372
+ const existing = responseBody[normalizedStatus];
373
+ responseBody[normalizedStatus] = existing === void 0 ? declaration : unionJsonSchemas(existing, declaration);
374
+ hasResponse = true;
375
+ }
376
+ }
377
+ if (!hasRequest && !hasResponse) return void 0;
378
+ return {
379
+ ...hasRequest ? { request } : {},
380
+ ...hasResponse ? { response: { body: responseBody } } : {}
381
+ };
382
+ }
383
+ //#endregion
384
+ //#region src/router/response/directives.ts
385
+ const routekitDirectiveBrand = Symbol("RoutekitDirective");
386
+ /**
387
+ * Test whether a value is a generator directive.
388
+ *
389
+ * @example
390
+ * ```ts
391
+ * if (isRoutekitDirective(value)) console.log(value.kind)
392
+ * ```
393
+ *
394
+ * @param value - Value to inspect.
395
+ * @returns Whether `value` is a Routekit generator directive.
396
+ */
397
+ function isRoutekitDirective(value) {
398
+ return !!value && value[routekitDirectiveBrand] === true;
399
+ }
400
+ /**
401
+ * Test whether a value is a status directive.
402
+ *
403
+ * @param value - Value to inspect.
404
+ * @returns Whether `value` was created by [`status`]{@link status}.
405
+ */
406
+ function isStatusDirective(value) {
407
+ return isRoutekitDirective(value) && value.kind === "status";
408
+ }
409
+ /**
410
+ * Test whether a value is a headers directive.
411
+ *
412
+ * @param value - Value to inspect.
413
+ * @returns Whether `value` was created by [`headers`]{@link headers}.
414
+ */
415
+ function isHeadersDirective(value) {
416
+ return isRoutekitDirective(value) && value.kind === "headers";
417
+ }
418
+ /**
419
+ * Test whether a value is a head directive.
420
+ *
421
+ * @param value - Value to inspect.
422
+ * @returns Whether `value` was created by [`head`]{@link head}.
423
+ */
424
+ function isHeadDirective(value) {
425
+ return isRoutekitDirective(value) && value.kind === "head";
426
+ }
427
+ /**
428
+ * Test whether a value is a stream directive.
429
+ *
430
+ * @param value - Value to inspect.
431
+ * @returns Whether `value` was created by [`stream`]{@link stream}.
432
+ */
433
+ function isStreamDirective(value) {
434
+ return isRoutekitDirective(value) && value.kind === "stream";
435
+ }
436
+ /**
437
+ * Test whether a value is a chunk directive.
438
+ *
439
+ * @param value - Value to inspect.
440
+ * @returns Whether `value` was created by [`chunk`]{@link chunk}.
441
+ */
442
+ function isChunkDirective(value) {
443
+ return isRoutekitDirective(value) && value.kind === "chunk";
444
+ }
445
+ function makeDirective(directive) {
446
+ return {
447
+ [routekitDirectiveBrand]: true,
448
+ ...directive
449
+ };
450
+ }
451
+ /**
452
+ * Create a generator directive that sets the response status.
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * yield status(HttpStatus.ACCEPTED)
457
+ * ```
458
+ *
459
+ * @param statusCode - HTTP status code to use.
460
+ * @returns Status directive.
461
+ */
462
+ function status(statusCode) {
463
+ return makeDirective({
464
+ kind: "status",
465
+ status: statusCode
466
+ });
467
+ }
468
+ /**
469
+ * Create a generator directive that merges response headers.
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * yield headers({'cache-control': 'no-store'})
474
+ * ```
475
+ *
476
+ * @param init - Headers to merge into the response.
477
+ * @returns Headers directive.
478
+ */
479
+ function headers(init) {
480
+ return makeDirective({
481
+ kind: "headers",
482
+ headers: new Headers(init)
483
+ });
484
+ }
485
+ /**
486
+ * Create a generator directive that sets response status and headers.
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * yield head(HttpStatus.OK, {'cache-control': 'no-store'})
491
+ * ```
492
+ *
493
+ * @param statusCode - HTTP status code to use.
494
+ * @param init - Headers to merge into the response.
495
+ * @returns Head directive.
496
+ */
497
+ function head(statusCode, init = {}) {
498
+ return makeDirective({
499
+ kind: "head",
500
+ status: statusCode,
501
+ headers: new Headers(init)
502
+ });
503
+ }
504
+ /**
505
+ * Create a generator directive that selects a structured stream framer.
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * yield stream(sseFramer())
510
+ * ```
511
+ *
512
+ * @param framer - Stream framer to use for subsequent chunks.
513
+ * @param init - Additional stream headers.
514
+ * @returns Stream directive.
515
+ */
516
+ function stream(framer, init = {}) {
517
+ const streamHeaders = new Headers(framer.headers);
518
+ new Headers(init).forEach((value, key) => streamHeaders.set(key, value));
519
+ return makeDirective({
520
+ kind: "stream",
521
+ framer,
522
+ headers: streamHeaders
523
+ });
524
+ }
525
+ /**
526
+ * Create a generator directive that emits one stream chunk.
527
+ *
528
+ * @example
529
+ * ```ts
530
+ * yield chunk({event: 'ready'})
531
+ * ```
532
+ *
533
+ * @param value - Chunk value to stream.
534
+ * @returns Chunk directive.
535
+ */
536
+ function chunk(value) {
537
+ return makeDirective({
538
+ kind: "chunk",
539
+ value
540
+ });
541
+ }
542
+ //#endregion
543
+ //#region src/router/response/framers.ts
544
+ /**
545
+ * Create an SSE stream framer.
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * yield stream(sseFramer())
550
+ * yield chunk({event: 'ready'})
551
+ * ```
552
+ *
553
+ * @returns Stream framer for `text/event-stream`.
554
+ */
555
+ function sseFramer() {
556
+ return {
557
+ contentType: "text/event-stream; charset=utf-8",
558
+ headers: {
559
+ [CommonHeaders.CACHE_CONTROL]: "no-cache",
560
+ [CommonHeaders.CONNECTION]: "keep-alive"
561
+ },
562
+ frame: (value) => `data: ${JSON.stringify(value) ?? "null"}\n\n`
563
+ };
564
+ }
565
+ /**
566
+ * Create a JSON Lines stream framer.
567
+ *
568
+ * @example
569
+ * ```ts
570
+ * yield stream(jsonLinesFramer())
571
+ * yield chunk({event: 'ready'})
572
+ * ```
573
+ *
574
+ * @returns Stream framer for `application/jsonl`.
575
+ */
576
+ function jsonLinesFramer() {
577
+ return {
578
+ contentType: "application/jsonl; charset=utf-8",
579
+ frame: (value) => `${JSON.stringify(value) ?? "null"}\n`
580
+ };
581
+ }
582
+ //#endregion
583
+ //#region src/router/response/serializers.ts
584
+ /**
585
+ * Create the default JSON response body serializer.
586
+ *
587
+ * @example
588
+ * ```ts
589
+ * const router = new Router().setResponseBodySerializers([jsonResponseBodySerializer()])
590
+ * ```
591
+ *
592
+ * @returns Serializer for `application/json`.
593
+ */
594
+ function jsonResponseBodySerializer() {
595
+ return {
596
+ mediaTypes: [CommonContentTypes.JSON],
597
+ canSerialize: (_value) => true,
598
+ serialize: (value) => value === void 0 ? null : JSON.stringify(value)
599
+ };
600
+ }
601
+ /**
602
+ * Create the default response body serializer list.
603
+ *
604
+ * @example
605
+ * ```ts
606
+ * const serializers = defaultResponseBodySerializers()
607
+ * ```
608
+ *
609
+ * @returns Default response body serializers.
610
+ */
611
+ function defaultResponseBodySerializers() {
612
+ return [jsonResponseBodySerializer()];
613
+ }
614
+ //#endregion
615
+ //#region src/router/response/stream.ts
616
+ function createStartStream(fn) {
617
+ return new ReadableStream({ start(controller) {
618
+ fn(controller);
619
+ } });
620
+ }
621
+ function createAsyncStream(fn) {
622
+ return new ReadableStream({ start(controller) {
623
+ const writer = { write: controller.enqueue.bind(controller) };
624
+ Promise.try(fn, writer).then(() => {
625
+ controller.close();
626
+ }, (err) => {
627
+ controller.error(err);
628
+ });
629
+ } });
630
+ }
631
+ //#endregion
632
+ //#region src/router/middleware/define.ts
633
+ const declaredMiddlewareBrand = Symbol("DeclaredMiddleware");
634
+ const middlewareActionBrand = Symbol("MiddlewareAction");
635
+ function declarationsSchema(declarations) {
636
+ if (!declarations) return void 0;
637
+ const body = Object.fromEntries(Object.entries(declarations).flatMap(([status, declaration]) => declaration ? [[status, declaration.schema]] : []));
638
+ return Object.keys(body).length > 0 ? { response: { body } } : void 0;
639
+ }
640
+ function middlewareAction(result) {
641
+ return {
642
+ [middlewareActionBrand]: true,
643
+ result
644
+ };
645
+ }
646
+ function defineMiddleware(options) {
647
+ const declarations = options.responses;
648
+ const schema = mergeRouteSchemas(options.schema, declarationsSchema(declarations));
649
+ const middleware = async (ctx, next) => {
650
+ const controls = {
651
+ next,
652
+ forward: middlewareAction,
653
+ respond(result) {
654
+ const declaration = declarations?.[result.status] ?? declarations?.default;
655
+ if (!declaration) throw new Error(`Middleware returned undeclared response status ${result.status}.`);
656
+ return middlewareAction(response(declaration.parse(result.body), {
657
+ status: result.status,
658
+ headers: result.headers
659
+ }));
660
+ }
661
+ };
662
+ const action = await options.run(ctx, controls);
663
+ if (action === void 0) return await next();
664
+ if (!action[middlewareActionBrand]) throw new TypeError("Declared middleware must return respond(...) or forward(...).");
665
+ return action.result;
666
+ };
667
+ Object.defineProperties(middleware, {
668
+ [declaredMiddlewareBrand]: { value: true },
669
+ schema: { value: schema },
670
+ schemaAppliesTo: { value: options.schemaAppliesTo }
671
+ });
672
+ return middleware;
673
+ }
674
+ /**
675
+ * Test whether a middleware value carries schema declarations.
676
+ *
677
+ * @param value - Middleware value to inspect.
678
+ * @returns Whether `value` was created by [`defineMiddleware`]{@link defineMiddleware}.
679
+ * @internal
680
+ */
681
+ function isDeclaredMiddleware(value) {
682
+ return typeof value === "function" && value[declaredMiddlewareBrand] === true;
683
+ }
684
+ //#endregion
685
+ //#region src/router/request.ts
686
+ /**
687
+ * Error thrown when Routekit cannot read or parse a request body.
688
+ *
689
+ * @example
690
+ * ```ts
691
+ * throw new RequestBodyError('Unsupported body', HttpStatus.UNSUPPORTED_MEDIA_TYPE)
692
+ * ```
693
+ */
694
+ var RequestBodyError = class extends Error {
695
+ /**
696
+ * HTTP status code that should be returned for this request body failure.
697
+ */
698
+ status;
699
+ /**
700
+ * Create a request body error.
701
+ *
702
+ * @param message - Error message.
703
+ * @param status - HTTP status code for the failure.
704
+ */
705
+ constructor(message, status) {
706
+ super(message);
707
+ this.name = "RequestBodyError";
708
+ this.status = status;
709
+ }
710
+ };
711
+ /**
712
+ * Error thrown when no registered parser accepts the request `Content-Type`.
713
+ *
714
+ * @example
715
+ * ```ts
716
+ * throw new UnsupportedRequestBodyMediaTypeError('image/png')
717
+ * ```
718
+ */
719
+ var UnsupportedRequestBodyMediaTypeError = class extends RequestBodyError {
720
+ /**
721
+ * Incoming `Content-Type` value that was rejected.
722
+ */
723
+ contentType;
724
+ /**
725
+ * Create an unsupported media type error.
726
+ *
727
+ * @param contentType - Incoming content type, when present.
728
+ */
729
+ constructor(contentType) {
730
+ super(contentType ? `Unsupported request body media type: ${contentType}` : "Request body is missing Content-Type.", HttpStatus.UNSUPPORTED_MEDIA_TYPE);
731
+ this.name = "UnsupportedRequestBodyMediaTypeError";
732
+ this.contentType = contentType;
733
+ }
734
+ };
735
+ /**
736
+ * Error thrown when a request body exceeds a configured byte limit.
737
+ *
738
+ * @example
739
+ * ```ts
740
+ * throw new RequestBodyTooLargeError(1024, 2048)
741
+ * ```
742
+ */
743
+ var RequestBodyTooLargeError = class extends RequestBodyError {
744
+ /**
745
+ * Maximum allowed body size in bytes.
746
+ */
747
+ maxSize;
748
+ /**
749
+ * Number of bytes received before the limit failed.
750
+ */
751
+ received;
752
+ /**
753
+ * Create a body-too-large error.
754
+ *
755
+ * @param maxSize - Maximum allowed body size in bytes.
756
+ * @param received - Number of bytes received.
757
+ */
758
+ constructor(maxSize, received) {
759
+ super(`Payload exceeded ${maxSize} bytes`, HttpStatus.PAYLOAD_TOO_LARGE);
760
+ this.name = "RequestBodyTooLargeError";
761
+ this.maxSize = maxSize;
762
+ this.received = received;
763
+ }
764
+ };
765
+ /**
766
+ * Error thrown when `Content-Length` does not match the streamed body size.
767
+ *
768
+ * @example
769
+ * ```ts
770
+ * throw new RequestBodyLengthMismatchError(4, 3)
771
+ * ```
772
+ */
773
+ var RequestBodyLengthMismatchError = class extends RequestBodyError {
774
+ /**
775
+ * Expected byte count from `Content-Length`.
776
+ */
777
+ expected;
778
+ /**
779
+ * Actual byte count read from the stream.
780
+ */
781
+ received;
782
+ /**
783
+ * Create a content-length mismatch error.
784
+ *
785
+ * @param expected - Expected byte count.
786
+ * @param received - Actual byte count.
787
+ */
788
+ constructor(expected, received) {
789
+ super(`Content-Length ${expected} bytes did not match ${received} bytes`, HttpStatus.BAD_REQUEST);
790
+ this.name = "RequestBodyLengthMismatchError";
791
+ this.expected = expected;
792
+ this.received = received;
793
+ }
794
+ };
795
+ var DefaultRoutekitRequestBody = class DefaultRoutekitRequestBody {
796
+ contentType;
797
+ #raw;
798
+ #headers;
799
+ #stream;
800
+ #parsers;
801
+ #arrayBufferPromise;
802
+ #textPromise;
803
+ #formDataPromise;
804
+ #parsePromise;
805
+ constructor(options) {
806
+ this.#raw = options.raw;
807
+ this.#headers = options.headers;
808
+ this.#stream = options.stream;
809
+ this.#parsers = options.parsers;
810
+ this.contentType = parseContentType(options.headers.get(CommonHeaders.CONTENT_TYPE));
811
+ }
812
+ get headers() {
813
+ return this.#headers;
814
+ }
815
+ stream() {
816
+ return this.#stream;
817
+ }
818
+ withStream(stream) {
819
+ return new DefaultRoutekitRequestBody({
820
+ raw: this.#raw,
821
+ headers: this.#headers,
822
+ stream,
823
+ parsers: this.#parsers
824
+ });
825
+ }
826
+ async parse() {
827
+ if (!this.#stream) return void 0;
828
+ this.#parsePromise ??= this.#parse();
829
+ return await this.#parsePromise;
830
+ }
831
+ async json() {
832
+ return JSON.parse(await this.text());
833
+ }
834
+ text() {
835
+ this.#textPromise ??= this.arrayBuffer().then((buffer) => new TextDecoder().decode(buffer));
836
+ return this.#textPromise;
837
+ }
838
+ arrayBuffer() {
839
+ this.#arrayBufferPromise ??= new Response(this.#stream).arrayBuffer();
840
+ return this.#arrayBufferPromise;
841
+ }
842
+ formData() {
843
+ return this.#formDataPromise ??= new Response(this.#stream, { headers: this.#headers }).formData();
844
+ }
845
+ async #parse() {
846
+ const parser = this.#selectParser();
847
+ if (!parser) throw new UnsupportedRequestBodyMediaTypeError(this.#headers.get(CommonHeaders.CONTENT_TYPE));
848
+ try {
849
+ return await parser.parse(this);
850
+ } catch (error) {
851
+ if (error instanceof RequestBodyError) throw error;
852
+ throw new RequestBodyError(error instanceof Error ? error.message : String(error), HttpStatus.BAD_REQUEST);
853
+ }
854
+ }
855
+ #parserQuality(parser) {
856
+ let bestQuality = parser.canParse?.(this.contentType) ? 1 : 0;
857
+ if (this.contentType) {
858
+ for (const mediaType of parser.mediaTypes) if (mediaRangeAccepts(mediaType, this.contentType)) bestQuality = Math.max(bestQuality, mediaRangeQuality(mediaType));
859
+ }
860
+ return bestQuality > 0 ? bestQuality : null;
861
+ }
862
+ #selectParser() {
863
+ let best = null;
864
+ for (const parser of this.#parsers) {
865
+ const quality = this.#parserQuality(parser);
866
+ if (quality == null) continue;
867
+ if (!best || quality > best.quality) best = {
868
+ parser,
869
+ quality
870
+ };
871
+ }
872
+ return best?.parser ?? null;
873
+ }
874
+ };
875
+ var DefaultRoutekitRequest = class DefaultRoutekitRequest {
876
+ raw;
877
+ url;
878
+ method;
879
+ headers;
880
+ signal;
881
+ body;
882
+ constructor(raw, url, body) {
883
+ this.raw = raw;
884
+ this.url = url;
885
+ this.method = raw.method;
886
+ this.headers = raw.headers;
887
+ this.signal = raw.signal;
888
+ this.body = body;
889
+ }
890
+ withBody(body) {
891
+ return new DefaultRoutekitRequest(this.raw, this.url, body);
892
+ }
893
+ };
894
+ function readParams(searchParams) {
895
+ const query = {};
896
+ for (const [key, value] of searchParams.entries()) {
897
+ const existing = query[key];
898
+ if (existing === void 0) {
899
+ query[key] = value;
900
+ continue;
901
+ }
902
+ if (Array.isArray(existing)) existing.push(value);
903
+ else query[key] = [existing, value];
904
+ }
905
+ return query;
906
+ }
907
+ /**
908
+ * Create the default JSON request body parser.
909
+ *
910
+ * @example
911
+ * ```ts
912
+ * const router = new Router().setRequestBodyParsers([jsonRequestBodyParser()])
913
+ * ```
914
+ *
915
+ * @returns Parser for `application/json` and `application/*+json` request bodies.
916
+ */
917
+ function jsonRequestBodyParser() {
918
+ return {
919
+ mediaTypes: ["application/json", "application/*+json;q=0.5"],
920
+ parse: async (ctx) => JSON.parse(await ctx.text())
921
+ };
922
+ }
923
+ /**
924
+ * Create the default text request body parser.
925
+ *
926
+ * @example
927
+ * ```ts
928
+ * const router = new Router().setRequestBodyParsers([textRequestBodyParser()])
929
+ * ```
930
+ *
931
+ * @returns Parser for `text/*` request bodies.
932
+ */
933
+ function textRequestBodyParser() {
934
+ return {
935
+ mediaTypes: ["text/*"],
936
+ parse: (ctx) => ctx.text()
937
+ };
938
+ }
939
+ /**
940
+ * Create the default URL-encoded form request body parser.
941
+ *
942
+ * @example
943
+ * ```ts
944
+ * const router = new Router().setRequestBodyParsers([urlEncodedRequestBodyParser()])
945
+ * ```
946
+ *
947
+ * @returns Parser for `application/x-www-form-urlencoded` request bodies.
948
+ */
949
+ function urlEncodedRequestBodyParser() {
950
+ return {
951
+ mediaTypes: ["application/x-www-form-urlencoded"],
952
+ parse: async (ctx) => readParams(new URLSearchParams(await ctx.text()))
953
+ };
954
+ }
955
+ /**
956
+ * Create the default multipart form request body parser.
957
+ *
958
+ * @example
959
+ * ```ts
960
+ * const router = new Router().setRequestBodyParsers([formDataRequestBodyParser()])
961
+ * ```
962
+ *
963
+ * @returns Parser for `multipart/form-data` request bodies.
964
+ */
965
+ function formDataRequestBodyParser() {
966
+ return {
967
+ mediaTypes: ["multipart/form-data"],
968
+ parse: (ctx) => ctx.formData()
969
+ };
970
+ }
971
+ /**
972
+ * Create the default Routekit request body parser list.
973
+ *
974
+ * @example
975
+ * ```ts
976
+ * const parsers = defaultRequestBodyParsers()
977
+ * ```
978
+ *
979
+ * @returns Default parser list.
980
+ */
981
+ function defaultRequestBodyParsers() {
982
+ return [
983
+ jsonRequestBodyParser(),
984
+ textRequestBodyParser(),
985
+ urlEncodedRequestBodyParser(),
986
+ formDataRequestBodyParser()
987
+ ];
988
+ }
989
+ /**
990
+ * Create a Routekit request wrapper for a native Fetch request.
991
+ *
992
+ * @example
993
+ * ```ts
994
+ * const request = createRoutekitRequest(new Request('https://example.com'), new URL('https://example.com'), defaultRequestBodyParsers())
995
+ * ```
996
+ *
997
+ * @param raw - Native Fetch request.
998
+ * @param url - Parsed request URL.
999
+ * @param parsers - Request body parsers.
1000
+ * @returns Routekit request wrapper.
1001
+ */
1002
+ function createRoutekitRequest(raw, url, parsers) {
1003
+ return new DefaultRoutekitRequest(raw, url, new DefaultRoutekitRequestBody({
1004
+ raw,
1005
+ headers: raw.headers,
1006
+ stream: raw.body,
1007
+ parsers
1008
+ }));
1009
+ }
1010
+ /**
1011
+ * Create a plain response for a request body error.
1012
+ *
1013
+ * @example
1014
+ * ```ts
1015
+ * const response = responseFromRequestBodyError(error)
1016
+ * ```
1017
+ *
1018
+ * @param error - Request body error to convert.
1019
+ * @returns HTTP response for the body error.
1020
+ */
1021
+ function responseFromRequestBodyError(error) {
1022
+ return new Response(StatusText[error.status] ?? error.message, { status: error.status });
1023
+ }
1024
+ //#endregion
1025
+ export { stream as A, parseMediaType as B, isChunkDirective as C, isStatusDirective as D, isRoutekitDirective as E, mediaRangeToContentType as F, mediaTypeMatches as I, normalizeMediaType as L, isJsonContentType as M, mediaRangeAccepts as N, isStreamDirective as O, mediaRangeQuality as P, parseAcceptHeader as R, headers as S, isHeadersDirective as T, jsonResponseBodySerializer as _, createRoutekitRequest as a, chunk as b, jsonRequestBodyParser as c, urlEncodedRequestBodyParser as d, defineMiddleware as f, defaultResponseBodySerializers as g, createStartStream as h, UnsupportedRequestBodyMediaTypeError as i, mergeRouteSchemas as j, status as k, responseFromRequestBodyError as l, createAsyncStream as m, RequestBodyLengthMismatchError as n, defaultRequestBodyParsers as o, isDeclaredMiddleware as p, RequestBodyTooLargeError as r, formDataRequestBodyParser as s, RequestBodyError as t, textRequestBodyParser as u, jsonLinesFramer as v, isHeadDirective as w, head as x, sseFramer as y, parseContentType as z };