@mpen/routekit 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.d.mts +4 -0
- package/dist/client/react.d.mts +178 -0
- package/dist/client/react.mjs +142 -0
- package/dist/client.d.mts +433 -0
- package/dist/client.mjs +264 -0
- package/dist/content-BuDOmhH_.mjs +102 -0
- package/dist/core-CzUCxvGk.d.mts +140 -0
- package/dist/core-DbmQauwS.mjs +81 -0
- package/dist/handlers.d.mts +72 -0
- package/dist/handlers.mjs +153 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1152 -0
- package/dist/middleware.d.mts +388 -0
- package/dist/middleware.mjs +1222 -0
- package/dist/request-Dn0zc-xm.mjs +1025 -0
- package/dist/response/content.d.mts +79 -0
- package/dist/response/content.mjs +2 -0
- package/dist/response/json-rpc.d.mts +1 -0
- package/dist/response/json-rpc.mjs +1 -0
- package/dist/response/problem/valibot.d.mts +230 -0
- package/dist/response/problem/valibot.mjs +258 -0
- package/dist/response/problem.d.mts +415 -0
- package/dist/response/problem.mjs +183 -0
- package/dist/response/status.d.mts +45 -0
- package/dist/response/status.mjs +2 -0
- package/dist/responses-B379Ep9Y.d.mts +296 -0
- package/dist/responses-BpVrgeYi.mjs +101 -0
- package/dist/router-Cwb7ak0J.d.mts +1819 -0
- package/dist/routes.d.mts +282 -0
- package/dist/routes.mjs +311 -0
- package/dist/status-C-8mw-FB.mjs +59 -0
- package/dist/valibot-D7liFYyB.d.mts +290 -0
- package/dist/valibot-Du97X-TS.mjs +326 -0
- package/package.json +8 -2
- package/src/bin/gen-api-client.test.ts +0 -70
- package/src/bin/gen-api-client.ts +0 -986
- package/src/client/headers.ts +0 -31
- package/src/client/index.ts +0 -8
- package/src/client/promise.ts +0 -11
- package/src/client/react/index.test.tsx +0 -266
- package/src/client/react/index.ts +0 -431
- package/src/client/responses.test.ts +0 -151
- package/src/client/responses.ts +0 -278
- package/src/client/transport.ts +0 -74
- package/src/client/transports/body-codec.ts +0 -61
- package/src/client/transports/fetch.ts +0 -113
- package/src/client/tsconfig.json +0 -9
- package/src/client/types.ts +0 -15
- package/src/client/url.ts +0 -31
- package/src/index.ts +0 -63
- package/src/router/fetch-types.ts +0 -13
- package/src/router/handlers/index.ts +0 -2
- package/src/router/handlers/openapi/index.ts +0 -2
- package/src/router/handlers/openapi/openapi.ts +0 -293
- package/src/router/integration/zod-openapi.test.ts +0 -74
- package/src/router/lib/charset.test.ts +0 -22
- package/src/router/lib/charset.ts +0 -133
- package/src/router/lib/collections.ts +0 -3
- package/src/router/lib/format.test.ts +0 -67
- package/src/router/lib/format.ts +0 -35
- package/src/router/lib/host.ts +0 -4
- package/src/router/lib/json-schema.ts +0 -6
- package/src/router/lib/media-type.test.ts +0 -122
- package/src/router/lib/media-type.ts +0 -289
- package/src/router/lib/pathname.test.ts +0 -18
- package/src/router/lib/pathname.ts +0 -19
- package/src/router/lib/route-names.ts +0 -70
- package/src/router/lib/route-normalize.test.ts +0 -36
- package/src/router/lib/route-normalize.ts +0 -67
- package/src/router/lib/schema-merge.ts +0 -56
- package/src/router/middleware/accept-ctx.test.ts +0 -33
- package/src/router/middleware/accept-ctx.ts +0 -12
- package/src/router/middleware/body-limit.test.ts +0 -112
- package/src/router/middleware/body-limit.ts +0 -121
- package/src/router/middleware/content-type-context.ts +0 -0
- package/src/router/middleware/cors.test.ts +0 -269
- package/src/router/middleware/cors.ts +0 -490
- package/src/router/middleware/csrf.test.ts +0 -106
- package/src/router/middleware/csrf.ts +0 -192
- package/src/router/middleware/define.ts +0 -249
- package/src/router/middleware/index.ts +0 -34
- package/src/router/middleware/jsxhtml-response.ts +0 -0
- package/src/router/middleware/oas-swagger.ts +0 -0
- package/src/router/middleware/rate-limit.test.ts +0 -886
- package/src/router/middleware/rate-limit.ts +0 -920
- package/src/router/middleware/request-id-ctx.test.ts +0 -183
- package/src/router/middleware/request-id-ctx.ts +0 -135
- package/src/router/middleware/request-logger-format.test.ts +0 -16
- package/src/router/middleware/request-logger-format.ts +0 -269
- package/src/router/middleware/request-logger.test.ts +0 -267
- package/src/router/middleware/request-logger.ts +0 -131
- package/src/router/middleware/start-time-ctx.ts +0 -5
- package/src/router/request.ts +0 -611
- package/src/router/response/core.ts +0 -181
- package/src/router/response/directives.ts +0 -233
- package/src/router/response/formats/content/bodyless.ts +0 -54
- package/src/router/response/formats/content/content.ts +0 -79
- package/src/router/response/formats/content/index.ts +0 -2
- package/src/router/response/formats/json-rpc/index.ts +0 -2
- package/src/router/response/formats/problem/badRequest.ts +0 -90
- package/src/router/response/formats/problem/conflict.ts +0 -90
- package/src/router/response/formats/problem/created.ts +0 -40
- package/src/router/response/formats/problem/index.ts +0 -27
- package/src/router/response/formats/problem/notFound.ts +0 -90
- package/src/router/response/formats/problem/permissionDenied.ts +0 -90
- package/src/router/response/formats/problem/problem.test.ts +0 -888
- package/src/router/response/formats/problem/rateLimited.ts +0 -90
- package/src/router/response/formats/problem/responses.ts +0 -219
- package/src/router/response/formats/problem/root-errors.ts +0 -48
- package/src/router/response/formats/problem/sessionExpired.ts +0 -90
- package/src/router/response/formats/problem/types.ts +0 -170
- package/src/router/response/formats/problem/unauthenticated.ts +0 -90
- package/src/router/response/formats/problem/valibot.ts +0 -410
- package/src/router/response/formats/status/index.ts +0 -1
- package/src/router/response/formats/status/responses.ts +0 -59
- package/src/router/response/formats/status/status.test.ts +0 -21
- package/src/router/response/framers.ts +0 -85
- package/src/router/response/index.ts +0 -28
- package/src/router/response/openapi.test.ts +0 -96
- package/src/router/response/openapi.ts +0 -1
- package/src/router/response/serializers.ts +0 -66
- package/src/router/response/stream.ts +0 -35
- package/src/router/router.test.ts +0 -1571
- package/src/router/router.ts +0 -1965
- package/src/router/routes/index.ts +0 -46
- package/src/router/routes/valibot/index.ts +0 -18
- package/src/router/routes/valibot/valibot.ts +0 -1393
- package/src/router/routes/valibot.test.ts +0 -286
- package/src/router/routes/zod/index.ts +0 -18
- package/src/router/routes/zod/zod.ts +0 -1318
- package/src/router/routes/zod.test.ts +0 -280
- package/src/router/server-interface.ts +0 -31
- package/src/router/types.ts +0 -657
|
@@ -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 };
|