h3 2.0.1-rc.2 → 2.0.1-rc.21
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/bin/h3.mjs +35 -0
- package/dist/THIRD-PARTY-LICENSES.md +70 -0
- package/dist/_entries/bun.d.mts +3 -8
- package/dist/_entries/bun.mjs +3 -9
- package/dist/_entries/cloudflare.d.mts +4 -9
- package/dist/_entries/cloudflare.mjs +3 -9
- package/dist/_entries/deno.d.mts +3 -8
- package/dist/_entries/deno.mjs +3 -9
- package/dist/_entries/generic.d.mts +3 -8
- package/dist/_entries/generic.mjs +3 -9
- package/dist/_entries/node.d.mts +3 -8
- package/dist/_entries/node.mjs +3 -12
- package/dist/_entries/service-worker.d.mts +3 -8
- package/dist/_entries/service-worker.mjs +3 -9
- package/dist/docs/0.guide/0.index/index.md +117 -0
- package/dist/docs/0.guide/1.basics/0.lifecycle.md +68 -0
- package/dist/docs/0.guide/1.basics/1.routing.md +92 -0
- package/dist/docs/0.guide/1.basics/2.middleware.md +97 -0
- package/dist/docs/0.guide/1.basics/3.handler.md +165 -0
- package/dist/docs/0.guide/1.basics/4.response.md +162 -0
- package/dist/docs/0.guide/1.basics/5.error.md +117 -0
- package/dist/docs/0.guide/1.basics/6.nested-apps.md +57 -0
- package/dist/docs/0.guide/2.api/0.h3.md +145 -0
- package/dist/docs/0.guide/2.api/1.h3event.md +113 -0
- package/dist/docs/0.guide/3.advanced/0.plugins.md +50 -0
- package/dist/docs/0.guide/3.advanced/1.websocket.md +124 -0
- package/dist/docs/0.guide/3.advanced/2.nightly.md +13 -0
- package/dist/docs/1.utils/0.index/index.md +46 -0
- package/dist/docs/1.utils/1.request.md +355 -0
- package/dist/docs/1.utils/2.response.md +144 -0
- package/dist/docs/1.utils/3.cookie.md +33 -0
- package/dist/docs/1.utils/4.security.md +109 -0
- package/dist/docs/1.utils/5.proxy.md +31 -0
- package/dist/docs/1.utils/6.mcp.md +71 -0
- package/dist/docs/1.utils/7.more.md +78 -0
- package/dist/docs/1.utils/8.community.md +42 -0
- package/dist/docs/2.examples/0.index/index.md +16 -0
- package/dist/docs/2.examples/1.handle-cookie.md +67 -0
- package/dist/docs/2.examples/2.handle-session.md +130 -0
- package/dist/docs/2.examples/3.serve-static-assets.md +66 -0
- package/dist/docs/2.examples/4.stream-response.md +76 -0
- package/dist/docs/2.examples/5.validate-data.md +193 -0
- package/dist/docs/3.migration/0.index/index.md +200 -0
- package/dist/docs/README.md +35 -0
- package/dist/{h3.mjs → h3-CRCltuUf.mjs} +915 -1218
- package/dist/h3-D76FUMrE.d.mts +833 -0
- package/dist/h3-DagAgogP.mjs +4 -0
- package/dist/{h3.d.mts → h3-DiSMXP1G.d.mts} +320 -656
- package/dist/tracing.d.mts +24 -0
- package/dist/tracing.mjs +76 -0
- package/package.json +56 -44
|
@@ -1,74 +1,66 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "./h3-DagAgogP.mjs";
|
|
2
|
+
import { NullProtoObj as EmptyObject, addRoute, createRouter, findRoute, removeRoute, routeToRegExp } from "rou3";
|
|
2
3
|
import { FastResponse, FastURL } from "srvx";
|
|
3
|
-
import { parse, parseSetCookie, serialize, splitSetCookieString } from "cookie-es";
|
|
4
|
-
|
|
5
|
-
//#region src/_entries/_common.ts
|
|
6
4
|
function freezeApp(app) {
|
|
7
5
|
app.config = Object.freeze(app.config);
|
|
8
|
-
app
|
|
6
|
+
app["~addRoute"] = () => {
|
|
9
7
|
throw new Error("Cannot add routes after the server init.");
|
|
10
8
|
};
|
|
11
9
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
function withLeadingSlash(path) {
|
|
11
|
+
if (!path || path === "/") return "/";
|
|
12
|
+
return path[0] === "/" ? path : `/${path}`;
|
|
13
|
+
}
|
|
14
|
+
function withoutTrailingSlash(path) {
|
|
15
|
+
if (!path || path === "/") return "/";
|
|
16
|
+
return path[path.length - 1] === "/" ? path.slice(0, -1) : path;
|
|
17
|
+
}
|
|
18
|
+
function withoutBase(input = "", base = "") {
|
|
19
|
+
if (!base || base === "/") return input;
|
|
20
|
+
const _base = withoutTrailingSlash(base);
|
|
21
|
+
if (!input.startsWith(_base) || input.length > _base.length && input[_base.length] !== "/") return input;
|
|
22
|
+
return "/" + input.slice(_base.length).replace(/^\/+/, "");
|
|
23
|
+
}
|
|
24
|
+
function decodePathname(pathname) {
|
|
25
|
+
return decodeURI(pathname.includes("%25") ? pathname.replace(/%25/g, "%2525") : pathname);
|
|
26
|
+
}
|
|
27
|
+
function resolveDotSegments(path) {
|
|
28
|
+
if (!path.includes(".") && !path.includes("%2")) return path;
|
|
29
|
+
const segments = path.replaceAll("\\", "/").split("/");
|
|
30
|
+
const resolved = [];
|
|
31
|
+
for (const segment of segments) {
|
|
32
|
+
const normalized = segment.replace(/%2e/gi, ".");
|
|
33
|
+
if (normalized === "..") {
|
|
34
|
+
if (resolved.length > 1) resolved.pop();
|
|
35
|
+
} else if (normalized !== ".") resolved.push(segment);
|
|
36
|
+
}
|
|
37
|
+
return resolved.join("/") || "/";
|
|
17
38
|
}
|
|
18
|
-
|
|
19
|
-
//#endregion
|
|
20
|
-
//#region src/event.ts
|
|
21
39
|
const kEventNS = "h3.internal.event.";
|
|
22
40
|
const kEventRes = /* @__PURE__ */ Symbol.for(`${kEventNS}res`);
|
|
23
41
|
const kEventResHeaders = /* @__PURE__ */ Symbol.for(`${kEventNS}res.headers`);
|
|
42
|
+
const kEventResErrHeaders = /* @__PURE__ */ Symbol.for(`${kEventNS}res.err.headers`);
|
|
24
43
|
var H3Event = class {
|
|
25
|
-
/**
|
|
26
|
-
* Access to the H3 application instance.
|
|
27
|
-
*/
|
|
28
44
|
app;
|
|
29
|
-
/**
|
|
30
|
-
* Incoming HTTP request info.
|
|
31
|
-
*
|
|
32
|
-
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
|
33
|
-
*/
|
|
34
45
|
req;
|
|
35
|
-
/**
|
|
36
|
-
* Access to the parsed request URL.
|
|
37
|
-
*
|
|
38
|
-
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/URL)
|
|
39
|
-
*/
|
|
40
46
|
url;
|
|
41
|
-
/**
|
|
42
|
-
* Event context.
|
|
43
|
-
*/
|
|
44
47
|
context;
|
|
45
|
-
/**
|
|
46
|
-
* @internal
|
|
47
|
-
*/
|
|
48
48
|
static __is_event__ = true;
|
|
49
49
|
constructor(req, context, app) {
|
|
50
50
|
this.context = context || req.context || new EmptyObject();
|
|
51
51
|
this.req = req;
|
|
52
52
|
this.app = app;
|
|
53
53
|
const _url = req._url;
|
|
54
|
-
|
|
54
|
+
const url = _url && _url instanceof URL ? _url : new FastURL(req.url);
|
|
55
|
+
if (url.pathname.includes("%")) url.pathname = decodePathname(url.pathname);
|
|
56
|
+
this.url = url;
|
|
55
57
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Prepared HTTP response.
|
|
58
|
-
*/
|
|
59
58
|
get res() {
|
|
60
59
|
return this[kEventRes] ||= new H3EventResponse();
|
|
61
60
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Access to runtime specific additional context.
|
|
64
|
-
*
|
|
65
|
-
*/
|
|
66
61
|
get runtime() {
|
|
67
62
|
return this.req.runtime;
|
|
68
63
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Tell the runtime about an ongoing operation that shouldn't close until the promise resolves.
|
|
71
|
-
*/
|
|
72
64
|
waitUntil(promise) {
|
|
73
65
|
this.req.waitUntil?.(promise);
|
|
74
66
|
}
|
|
@@ -78,38 +70,15 @@ var H3Event = class {
|
|
|
78
70
|
toJSON() {
|
|
79
71
|
return this.toString();
|
|
80
72
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Access to the raw Node.js req/res objects.
|
|
83
|
-
*
|
|
84
|
-
* @deprecated Use `event.runtime.{node|deno|bun|...}.` instead.
|
|
85
|
-
*/
|
|
86
73
|
get node() {
|
|
87
74
|
return this.req.runtime?.node;
|
|
88
75
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Access to the incoming request headers.
|
|
91
|
-
*
|
|
92
|
-
* @deprecated Use `event.req.headers` instead.
|
|
93
|
-
*
|
|
94
|
-
*/
|
|
95
76
|
get headers() {
|
|
96
77
|
return this.req.headers;
|
|
97
78
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Access to the incoming request url (pathname+search).
|
|
100
|
-
*
|
|
101
|
-
* @deprecated Use `event.url.pathname + event.url.search` instead.
|
|
102
|
-
*
|
|
103
|
-
* Example: `/api/hello?name=world`
|
|
104
|
-
* */
|
|
105
79
|
get path() {
|
|
106
80
|
return this.url.pathname + this.url.search;
|
|
107
81
|
}
|
|
108
|
-
/**
|
|
109
|
-
* Access to the incoming request method.
|
|
110
|
-
*
|
|
111
|
-
* @deprecated Use `event.req.method` instead.
|
|
112
|
-
*/
|
|
113
82
|
get method() {
|
|
114
83
|
return this.req.method;
|
|
115
84
|
}
|
|
@@ -120,90 +89,34 @@ var H3EventResponse = class {
|
|
|
120
89
|
get headers() {
|
|
121
90
|
return this[kEventResHeaders] ||= new Headers();
|
|
122
91
|
}
|
|
92
|
+
get errHeaders() {
|
|
93
|
+
return this[kEventResErrHeaders] ||= new Headers();
|
|
94
|
+
}
|
|
123
95
|
};
|
|
124
|
-
|
|
125
|
-
//#endregion
|
|
126
|
-
//#region src/utils/sanitize.ts
|
|
127
96
|
const DISALLOWED_STATUS_CHARS = /[^\u0009\u0020-\u007E]/g;
|
|
128
|
-
/**
|
|
129
|
-
* Make sure the status message is safe to use in a response.
|
|
130
|
-
*
|
|
131
|
-
* Allowed characters: horizontal tabs, spaces or visible ascii characters: https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2
|
|
132
|
-
*/
|
|
133
97
|
function sanitizeStatusMessage(statusMessage = "") {
|
|
134
98
|
return statusMessage.replace(DISALLOWED_STATUS_CHARS, "");
|
|
135
99
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Make sure the status code is a valid HTTP status code.
|
|
138
|
-
*/
|
|
139
100
|
function sanitizeStatusCode(statusCode, defaultStatusCode = 200) {
|
|
140
101
|
if (!statusCode) return defaultStatusCode;
|
|
141
102
|
if (typeof statusCode === "string") statusCode = +statusCode;
|
|
142
103
|
if (statusCode < 100 || statusCode > 599) return defaultStatusCode;
|
|
143
104
|
return statusCode;
|
|
144
105
|
}
|
|
145
|
-
|
|
146
|
-
//#endregion
|
|
147
|
-
//#region src/error.ts
|
|
148
|
-
/**
|
|
149
|
-
* HTTPError
|
|
150
|
-
*/
|
|
151
106
|
var HTTPError = class HTTPError extends Error {
|
|
152
107
|
get name() {
|
|
153
108
|
return "HTTPError";
|
|
154
109
|
}
|
|
155
|
-
/**
|
|
156
|
-
* HTTP status code in range [200...599]
|
|
157
|
-
*/
|
|
158
110
|
status;
|
|
159
|
-
/**
|
|
160
|
-
* HTTP status text
|
|
161
|
-
*
|
|
162
|
-
* **NOTE:** This should be short (max 512 to 1024 characters).
|
|
163
|
-
* Allowed characters are tabs, spaces, visible ASCII characters, and extended characters (byte value 128–255).
|
|
164
|
-
*
|
|
165
|
-
* **TIP:** Use `message` for longer error descriptions in JSON body.
|
|
166
|
-
*/
|
|
167
111
|
statusText;
|
|
168
|
-
/**
|
|
169
|
-
* Additional HTTP headers to be sent in error response.
|
|
170
|
-
*/
|
|
171
112
|
headers;
|
|
172
|
-
/**
|
|
173
|
-
* Original error object that caused this error.
|
|
174
|
-
*/
|
|
175
113
|
cause;
|
|
176
|
-
/**
|
|
177
|
-
* Additional data attached in the error JSON body under `data` key.
|
|
178
|
-
*/
|
|
179
114
|
data;
|
|
180
|
-
/**
|
|
181
|
-
* Additional top level JSON body properties to attach in the error JSON body.
|
|
182
|
-
*/
|
|
183
115
|
body;
|
|
184
|
-
/**
|
|
185
|
-
* Flag to indicate that the error was not handled by the application.
|
|
186
|
-
*
|
|
187
|
-
* Unhandled error stack trace, data and message are hidden in non debug mode for security reasons.
|
|
188
|
-
*/
|
|
189
116
|
unhandled;
|
|
190
|
-
/**
|
|
191
|
-
* Check if the input is an instance of HTTPError using its constructor name.
|
|
192
|
-
*
|
|
193
|
-
* It is safer than using `instanceof` because it works across different contexts (e.g., if the error was thrown in a different module).
|
|
194
|
-
*/
|
|
195
117
|
static isError(input) {
|
|
196
118
|
return input instanceof Error && input?.name === "HTTPError";
|
|
197
119
|
}
|
|
198
|
-
/**
|
|
199
|
-
* Create a new HTTPError with the given status code and optional status text and details.
|
|
200
|
-
*
|
|
201
|
-
* @example
|
|
202
|
-
*
|
|
203
|
-
* HTTPError.status(404)
|
|
204
|
-
* HTTPError.status(418, "I'm a teapot")
|
|
205
|
-
* HTTPError.status(403, "Forbidden", { message: "Not authenticated" })
|
|
206
|
-
*/
|
|
207
120
|
static status(status, statusText, details) {
|
|
208
121
|
return new HTTPError({
|
|
209
122
|
...details,
|
|
@@ -218,8 +131,8 @@ var HTTPError = class HTTPError extends Error {
|
|
|
218
131
|
messageInput = arg1;
|
|
219
132
|
details = arg2;
|
|
220
133
|
} else details = arg1;
|
|
221
|
-
const status = sanitizeStatusCode(details?.status ||
|
|
222
|
-
const statusText = sanitizeStatusMessage(details?.statusText ||
|
|
134
|
+
const status = sanitizeStatusCode(details?.status || details?.statusCode || (details?.cause)?.status || (details?.cause)?.statusCode, 500);
|
|
135
|
+
const statusText = sanitizeStatusMessage(details?.statusText || details?.statusMessage || (details?.cause)?.statusText || (details?.cause)?.statusMessage);
|
|
223
136
|
const message = messageInput || details?.message || (details?.cause)?.message || details?.statusText || details?.statusMessage || [
|
|
224
137
|
"HTTPError",
|
|
225
138
|
status,
|
|
@@ -227,7 +140,6 @@ var HTTPError = class HTTPError extends Error {
|
|
|
227
140
|
].filter(Boolean).join(" ");
|
|
228
141
|
super(message, { cause: details });
|
|
229
142
|
this.cause = details;
|
|
230
|
-
Error.captureStackTrace?.(this, this.constructor);
|
|
231
143
|
this.status = status;
|
|
232
144
|
this.statusText = statusText || void 0;
|
|
233
145
|
const rawHeaders = details?.headers || (details?.cause)?.headers;
|
|
@@ -236,15 +148,9 @@ var HTTPError = class HTTPError extends Error {
|
|
|
236
148
|
this.data = details?.data;
|
|
237
149
|
this.body = details?.body;
|
|
238
150
|
}
|
|
239
|
-
/**
|
|
240
|
-
* @deprecated Use `status`
|
|
241
|
-
*/
|
|
242
151
|
get statusCode() {
|
|
243
152
|
return this.status;
|
|
244
153
|
}
|
|
245
|
-
/**
|
|
246
|
-
* @deprecated Use `statusText`
|
|
247
|
-
*/
|
|
248
154
|
get statusMessage() {
|
|
249
155
|
return this.statusText;
|
|
250
156
|
}
|
|
@@ -260,15 +166,6 @@ var HTTPError = class HTTPError extends Error {
|
|
|
260
166
|
};
|
|
261
167
|
}
|
|
262
168
|
};
|
|
263
|
-
|
|
264
|
-
//#endregion
|
|
265
|
-
//#region src/utils/internal/object.ts
|
|
266
|
-
/**
|
|
267
|
-
* Checks if a certain input has a given property.
|
|
268
|
-
* @param obj - The input to check.
|
|
269
|
-
* @param prop - The property to check for.
|
|
270
|
-
* @returns A boolean indicating whether the input is an object and has the property.
|
|
271
|
-
*/
|
|
272
169
|
function hasProp(obj, prop) {
|
|
273
170
|
try {
|
|
274
171
|
return prop in obj;
|
|
@@ -286,17 +183,14 @@ function isJSONSerializable(value, _type) {
|
|
|
286
183
|
const proto = Object.getPrototypeOf(value);
|
|
287
184
|
return proto === Object.prototype || proto === null;
|
|
288
185
|
}
|
|
289
|
-
|
|
290
|
-
//#endregion
|
|
291
|
-
//#region src/response.ts
|
|
292
186
|
const kNotFound = /* @__PURE__ */ Symbol.for("h3.notFound");
|
|
293
187
|
const kHandled = /* @__PURE__ */ Symbol.for("h3.handled");
|
|
294
188
|
function toResponse(val, event, config = {}) {
|
|
295
|
-
if (typeof val?.then === "function") return
|
|
189
|
+
if (typeof val?.then === "function") return val.then((resolvedVal) => toResponse(resolvedVal, event, config), (r) => toResponse(typeof r === "number" ? new HTTPError({ status: r }) : r, event, config));
|
|
296
190
|
const response = prepareResponse(val, event, config);
|
|
297
191
|
if (typeof response?.then === "function") return toResponse(response, event, config);
|
|
298
|
-
const { onResponse
|
|
299
|
-
return onResponse
|
|
192
|
+
const { onResponse } = config;
|
|
193
|
+
return onResponse ? Promise.resolve(onResponse(response, event)).then(() => response) : response;
|
|
300
194
|
}
|
|
301
195
|
var HTTPResponse = class {
|
|
302
196
|
#headers;
|
|
@@ -330,11 +224,13 @@ function prepareResponse(val, event, config, nested) {
|
|
|
330
224
|
if (val?.stack) error.stack = val.stack;
|
|
331
225
|
}
|
|
332
226
|
if (error.unhandled && !config.silent) console.error(error);
|
|
333
|
-
const { onError
|
|
334
|
-
|
|
227
|
+
const { onError } = config;
|
|
228
|
+
const errHeaders = event[kEventRes]?.[kEventResErrHeaders];
|
|
229
|
+
return onError && !nested ? Promise.resolve(onError(error, event)).catch((error) => error).then((newVal) => prepareResponse(newVal ?? val, event, config, true)) : errorResponse(error, config.debug, errHeaders);
|
|
335
230
|
}
|
|
336
231
|
const preparedRes = event[kEventRes];
|
|
337
232
|
const preparedHeaders = preparedRes?.[kEventResHeaders];
|
|
233
|
+
event[kEventRes] = void 0;
|
|
338
234
|
if (!(val instanceof Response)) {
|
|
339
235
|
const res = prepareResponseBody(val, event, config);
|
|
340
236
|
const status = res.status || preparedRes?.status;
|
|
@@ -344,7 +240,7 @@ function prepareResponse(val, event, config, nested) {
|
|
|
344
240
|
headers: res.headers && preparedHeaders ? mergeHeaders$1(res.headers, preparedHeaders) : res.headers || preparedHeaders
|
|
345
241
|
});
|
|
346
242
|
}
|
|
347
|
-
if (!preparedHeaders) return val;
|
|
243
|
+
if (!preparedHeaders || nested || !val.ok) return val;
|
|
348
244
|
try {
|
|
349
245
|
mergeHeaders$1(val.headers, preparedHeaders, val.headers);
|
|
350
246
|
return val;
|
|
@@ -361,8 +257,16 @@ function mergeHeaders$1(base, overrides, target = new Headers(base)) {
|
|
|
361
257
|
else target.set(name, value);
|
|
362
258
|
return target;
|
|
363
259
|
}
|
|
364
|
-
const
|
|
365
|
-
|
|
260
|
+
const frozen = (name) => (...args) => {
|
|
261
|
+
throw new Error(`Headers are frozen (${name} ${args.join(", ")})`);
|
|
262
|
+
};
|
|
263
|
+
var FrozenHeaders = class extends Headers {
|
|
264
|
+
set = frozen("set");
|
|
265
|
+
append = frozen("append");
|
|
266
|
+
delete = frozen("delete");
|
|
267
|
+
};
|
|
268
|
+
const emptyHeaders = /* @__PURE__ */ new FrozenHeaders({ "content-length": "0" });
|
|
269
|
+
const jsonHeaders = /* @__PURE__ */ new FrozenHeaders({ "content-type": "application/json;charset=UTF-8" });
|
|
366
270
|
function prepareResponseBody(val, event, config) {
|
|
367
271
|
if (val === null || val === void 0) return {
|
|
368
272
|
body: "",
|
|
@@ -405,19 +309,18 @@ function prepareResponseBody(val, event, config) {
|
|
|
405
309
|
function nullBody(method, status) {
|
|
406
310
|
return method === "HEAD" || status === 100 || status === 101 || status === 102 || status === 204 || status === 205 || status === 304;
|
|
407
311
|
}
|
|
408
|
-
function errorResponse(error, debug) {
|
|
312
|
+
function errorResponse(error, debug, errHeaders) {
|
|
313
|
+
let headers = error.headers ? mergeHeaders$1(jsonHeaders, error.headers) : new Headers(jsonHeaders);
|
|
314
|
+
if (errHeaders) headers = mergeHeaders$1(headers, errHeaders);
|
|
409
315
|
return new FastResponse(JSON.stringify({
|
|
410
316
|
...error.toJSON(),
|
|
411
317
|
stack: debug && error.stack ? error.stack.split("\n").map((l) => l.trim()) : void 0
|
|
412
318
|
}, void 0, debug ? 2 : void 0), {
|
|
413
319
|
status: error.status,
|
|
414
320
|
statusText: error.statusText,
|
|
415
|
-
headers
|
|
321
|
+
headers
|
|
416
322
|
});
|
|
417
323
|
}
|
|
418
|
-
|
|
419
|
-
//#endregion
|
|
420
|
-
//#region src/middleware.ts
|
|
421
324
|
function defineMiddleware(input) {
|
|
422
325
|
return input;
|
|
423
326
|
}
|
|
@@ -431,7 +334,7 @@ function normalizeMiddleware(input, opts = {}) {
|
|
|
431
334
|
};
|
|
432
335
|
}
|
|
433
336
|
function createMatcher(opts) {
|
|
434
|
-
if (!opts.route && !opts.method && !opts.match) return
|
|
337
|
+
if (!opts.route && !opts.method && !opts.match) return;
|
|
435
338
|
const routeMatcher = opts.route ? routeToRegExp(opts.route) : void 0;
|
|
436
339
|
const method = opts.method?.toUpperCase();
|
|
437
340
|
return function _middlewareMatcher(event) {
|
|
@@ -459,14 +362,34 @@ function callMiddleware(event, middleware, handler, index = 0) {
|
|
|
459
362
|
return nextResult;
|
|
460
363
|
};
|
|
461
364
|
const ret = fn(event, next);
|
|
462
|
-
return
|
|
365
|
+
return isUnhandledResponse(ret) ? next() : typeof ret?.then === "function" ? ret.then((resolved) => isUnhandledResponse(resolved) ? next() : resolved) : ret;
|
|
366
|
+
}
|
|
367
|
+
function isUnhandledResponse(val) {
|
|
368
|
+
return val === void 0 || val === kNotFound;
|
|
369
|
+
}
|
|
370
|
+
function toMiddleware(input) {
|
|
371
|
+
let h = input.handler || input;
|
|
372
|
+
let isFunction = typeof h === "function";
|
|
373
|
+
if (!isFunction && typeof input?.fetch === "function") {
|
|
374
|
+
isFunction = true;
|
|
375
|
+
h = function _fetchHandler(event) {
|
|
376
|
+
return input.fetch(event.req);
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (!isFunction) return function noopMiddleware(event, next) {
|
|
380
|
+
return next();
|
|
381
|
+
};
|
|
382
|
+
if (h.length === 2) return h;
|
|
383
|
+
return function _middlewareHandler(event, next) {
|
|
384
|
+
const res = h(event);
|
|
385
|
+
return typeof res?.then === "function" ? res.then((r) => {
|
|
386
|
+
return is404(r) ? next() : r;
|
|
387
|
+
}) : is404(res) ? next() : res;
|
|
388
|
+
};
|
|
463
389
|
}
|
|
464
390
|
function is404(val) {
|
|
465
|
-
return val
|
|
391
|
+
return isUnhandledResponse(val) || val?.status === 404 && val instanceof Response;
|
|
466
392
|
}
|
|
467
|
-
|
|
468
|
-
//#endregion
|
|
469
|
-
//#region src/utils/internal/query.ts
|
|
470
393
|
const plusRegex = /\+/g;
|
|
471
394
|
function parseQuery(input) {
|
|
472
395
|
const params = new EmptyObject();
|
|
@@ -485,7 +408,7 @@ function parseQuery(input) {
|
|
|
485
408
|
for (let i = 0; i < inputLength + 1; i++) {
|
|
486
409
|
c = i === inputLength ? 38 : input.charCodeAt(i);
|
|
487
410
|
switch (c) {
|
|
488
|
-
case 38:
|
|
411
|
+
case 38:
|
|
489
412
|
hasBothKeyValuePair = equalityIndex > startingIndex;
|
|
490
413
|
if (!hasBothKeyValuePair) equalityIndex = i;
|
|
491
414
|
key = input.slice(startingIndex + 1, equalityIndex);
|
|
@@ -514,41 +437,35 @@ function parseQuery(input) {
|
|
|
514
437
|
keyHasPlus = false;
|
|
515
438
|
valueHasPlus = false;
|
|
516
439
|
break;
|
|
517
|
-
|
|
518
|
-
case 61: {
|
|
440
|
+
case 61:
|
|
519
441
|
if (equalityIndex <= startingIndex) equalityIndex = i;
|
|
520
442
|
else shouldDecodeValue = true;
|
|
521
443
|
break;
|
|
522
|
-
|
|
523
|
-
case 43: {
|
|
444
|
+
case 43:
|
|
524
445
|
if (equalityIndex > startingIndex) valueHasPlus = true;
|
|
525
446
|
else keyHasPlus = true;
|
|
526
447
|
break;
|
|
527
|
-
|
|
528
|
-
case 37: {
|
|
448
|
+
case 37:
|
|
529
449
|
if (equalityIndex > startingIndex) shouldDecodeValue = true;
|
|
530
450
|
else shouldDecodeKey = true;
|
|
531
451
|
break;
|
|
532
|
-
}
|
|
533
452
|
}
|
|
534
453
|
}
|
|
535
454
|
return params;
|
|
536
455
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
//#region src/utils/internal/validate.ts
|
|
540
|
-
async function validateData(data, fn) {
|
|
456
|
+
const VALIDATION_FAILED = "Validation failed";
|
|
457
|
+
async function validateData(data, fn, options) {
|
|
541
458
|
if ("~standard" in fn) {
|
|
542
459
|
const result = await fn["~standard"].validate(data);
|
|
543
|
-
if (result.issues) throw createValidationError({
|
|
544
|
-
message:
|
|
460
|
+
if (result.issues) throw createValidationError(options?.onError?.(result) || {
|
|
461
|
+
message: VALIDATION_FAILED,
|
|
545
462
|
issues: result.issues
|
|
546
463
|
});
|
|
547
464
|
return result.value;
|
|
548
465
|
}
|
|
549
466
|
try {
|
|
550
467
|
const res = await fn(data);
|
|
551
|
-
if (res === false) throw createValidationError({ message:
|
|
468
|
+
if (res === false) throw createValidationError(options?.onError?.({ issues: [{ message: VALIDATION_FAILED }] }) || { message: VALIDATION_FAILED });
|
|
552
469
|
if (res === true) return data;
|
|
553
470
|
return res ?? data;
|
|
554
471
|
} catch (error) {
|
|
@@ -563,14 +480,23 @@ const reqBodyKeys = new Set([
|
|
|
563
480
|
]);
|
|
564
481
|
function validatedRequest(req, validate) {
|
|
565
482
|
if (validate.headers) {
|
|
566
|
-
const validatedheaders = syncValidate("headers", Object.fromEntries(req.headers.entries()), validate.headers);
|
|
483
|
+
const validatedheaders = syncValidate("headers", Object.fromEntries(req.headers.entries()), validate.headers, validate.onError);
|
|
567
484
|
for (const [key, value] of Object.entries(validatedheaders)) req.headers.set(key, value);
|
|
568
485
|
}
|
|
569
486
|
if (!validate.body) return req;
|
|
570
487
|
return new Proxy(req, { get(_target, prop) {
|
|
571
488
|
if (validate.body) {
|
|
572
489
|
if (prop === "json") return function _validatedJson() {
|
|
573
|
-
return req.json().then((data) => validate.body["~standard"].validate(data)).then((result) =>
|
|
490
|
+
return req.json().then((data) => validate.body["~standard"].validate(data)).then((result) => {
|
|
491
|
+
if (result.issues) throw createValidationError(validate.onError?.({
|
|
492
|
+
_source: "body",
|
|
493
|
+
...result
|
|
494
|
+
}) || {
|
|
495
|
+
message: VALIDATION_FAILED,
|
|
496
|
+
issues: result.issues
|
|
497
|
+
});
|
|
498
|
+
return result.value;
|
|
499
|
+
});
|
|
574
500
|
};
|
|
575
501
|
else if (reqBodyKeys.has(prop)) throw new TypeError(`Cannot access .${prop} on request with JSON validation enabled. Use .json() instead.`);
|
|
576
502
|
}
|
|
@@ -579,48 +505,40 @@ function validatedRequest(req, validate) {
|
|
|
579
505
|
}
|
|
580
506
|
function validatedURL(url, validate) {
|
|
581
507
|
if (!validate.query) return url;
|
|
582
|
-
const validatedQuery = syncValidate("query", Object.fromEntries(url.searchParams.entries()), validate.query);
|
|
508
|
+
const validatedQuery = syncValidate("query", Object.fromEntries(url.searchParams.entries()), validate.query, validate.onError);
|
|
583
509
|
for (const [key, value] of Object.entries(validatedQuery)) url.searchParams.set(key, value);
|
|
584
510
|
return url;
|
|
585
511
|
}
|
|
586
|
-
function syncValidate(
|
|
512
|
+
function syncValidate(source, data, fn, onError) {
|
|
587
513
|
const result = fn["~standard"].validate(data);
|
|
588
|
-
if (result instanceof Promise) throw new TypeError(`Asynchronous validation is not supported for ${
|
|
589
|
-
if (result.issues) throw createValidationError({
|
|
514
|
+
if (result instanceof Promise) throw new TypeError(`Asynchronous validation is not supported for ${source}`);
|
|
515
|
+
if (result.issues) throw createValidationError(onError?.({
|
|
516
|
+
_source: source,
|
|
517
|
+
...result
|
|
518
|
+
}) || {
|
|
519
|
+
message: VALIDATION_FAILED,
|
|
520
|
+
issues: result.issues
|
|
521
|
+
});
|
|
590
522
|
return result.value;
|
|
591
523
|
}
|
|
592
|
-
function createValidationError(
|
|
593
|
-
return new HTTPError({
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
524
|
+
function createValidationError(cause) {
|
|
525
|
+
return HTTPError.isError(cause) ? cause : new HTTPError({
|
|
526
|
+
cause,
|
|
527
|
+
status: cause?.status || 400,
|
|
528
|
+
statusText: cause?.statusText || VALIDATION_FAILED,
|
|
529
|
+
message: cause?.message || VALIDATION_FAILED,
|
|
530
|
+
data: {
|
|
531
|
+
issues: cause?.issues,
|
|
532
|
+
message: cause instanceof Error ? VALIDATION_FAILED : cause?.message || VALIDATION_FAILED
|
|
533
|
+
}
|
|
599
534
|
});
|
|
600
535
|
}
|
|
601
|
-
|
|
602
|
-
//#endregion
|
|
603
|
-
//#region src/utils/event.ts
|
|
604
|
-
/**
|
|
605
|
-
* Checks if the input is an H3Event object.
|
|
606
|
-
* @param input - The input to check.
|
|
607
|
-
* @returns True if the input is an H3Event object, false otherwise.
|
|
608
|
-
* @see H3Event
|
|
609
|
-
*/
|
|
610
536
|
function isEvent(input) {
|
|
611
537
|
return input instanceof H3Event || input?.constructor?.__is_event__;
|
|
612
538
|
}
|
|
613
|
-
/**
|
|
614
|
-
* Checks if the input is an object with `{ req: Request }` signature.
|
|
615
|
-
* @param input - The input to check.
|
|
616
|
-
* @returns True if the input is is `{ req: Request }`
|
|
617
|
-
*/
|
|
618
539
|
function isHTTPEvent(input) {
|
|
619
540
|
return input?.req instanceof Request;
|
|
620
541
|
}
|
|
621
|
-
/**
|
|
622
|
-
* Gets the context of the event, if it does not exists, initializes a new context on `req.context`.
|
|
623
|
-
*/
|
|
624
542
|
function getEventContext(event) {
|
|
625
543
|
if (event.context) return event.context;
|
|
626
544
|
event.req.context ??= {};
|
|
@@ -628,6 +546,7 @@ function getEventContext(event) {
|
|
|
628
546
|
}
|
|
629
547
|
function mockEvent(_request, options) {
|
|
630
548
|
let request;
|
|
549
|
+
if (options?.body && !options.duplex) options.duplex = "half";
|
|
631
550
|
if (typeof _request === "string") {
|
|
632
551
|
let url = _request;
|
|
633
552
|
if (url[0] === "/") url = `http://localhost${url}`;
|
|
@@ -636,145 +555,53 @@ function mockEvent(_request, options) {
|
|
|
636
555
|
else request = _request;
|
|
637
556
|
return new H3Event(request);
|
|
638
557
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
558
|
+
function requestWithURL(req, url) {
|
|
559
|
+
const cache = { url };
|
|
560
|
+
return new Proxy(req, { get(target, prop) {
|
|
561
|
+
if (prop in cache) return cache[prop];
|
|
562
|
+
const value = Reflect.get(target, prop);
|
|
563
|
+
cache[prop] = typeof value === "function" ? value.bind(target) : value;
|
|
564
|
+
return cache[prop];
|
|
565
|
+
} });
|
|
566
|
+
}
|
|
567
|
+
function requestWithBaseURL(req, base) {
|
|
568
|
+
const url = new URL(req.url);
|
|
569
|
+
url.pathname = decodePathname(url.pathname).slice(base.length) || "/";
|
|
570
|
+
return requestWithURL(req, url.href);
|
|
571
|
+
}
|
|
649
572
|
function toRequest(input, options) {
|
|
650
573
|
if (typeof input === "string") {
|
|
651
574
|
let url = input;
|
|
652
575
|
if (url[0] === "/") {
|
|
653
576
|
const headers = options?.headers ? new Headers(options.headers) : void 0;
|
|
654
577
|
const host = headers?.get("host") || "localhost";
|
|
655
|
-
|
|
656
|
-
url = `${proto}://${host}${url}`;
|
|
578
|
+
url = `${headers?.get("x-forwarded-proto") === "https" ? "https" : "http"}://${host}${url}`;
|
|
657
579
|
}
|
|
658
580
|
return new Request(url, options);
|
|
659
581
|
} else if (options || input instanceof URL) return new Request(input, options);
|
|
660
582
|
return input;
|
|
661
583
|
}
|
|
662
|
-
/**
|
|
663
|
-
* Get parsed query string object from the request URL.
|
|
664
|
-
*
|
|
665
|
-
* @example
|
|
666
|
-
* app.get("/", (event) => {
|
|
667
|
-
* const query = getQuery(event); // { key: "value", key2: ["value1", "value2"] }
|
|
668
|
-
* });
|
|
669
|
-
*/
|
|
670
584
|
function getQuery(event) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
*
|
|
677
|
-
* You can use a simple function to validate the query object or use a Standard-Schema compatible library like `zod` to define a schema.
|
|
678
|
-
*
|
|
679
|
-
* @example
|
|
680
|
-
* app.get("/", async (event) => {
|
|
681
|
-
* const query = await getValidatedQuery(event, (data) => {
|
|
682
|
-
* return "key" in data && typeof data.key === "string";
|
|
683
|
-
* });
|
|
684
|
-
* });
|
|
685
|
-
* @example
|
|
686
|
-
* import { z } from "zod";
|
|
687
|
-
*
|
|
688
|
-
* app.get("/", async (event) => {
|
|
689
|
-
* const query = await getValidatedQuery(
|
|
690
|
-
* event,
|
|
691
|
-
* z.object({
|
|
692
|
-
* key: z.string(),
|
|
693
|
-
* }),
|
|
694
|
-
* );
|
|
695
|
-
* });
|
|
696
|
-
*/
|
|
697
|
-
function getValidatedQuery(event, validate) {
|
|
698
|
-
const query = getQuery(event);
|
|
699
|
-
return validateData(query, validate);
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Get matched route params.
|
|
703
|
-
*
|
|
704
|
-
* If `decode` option is `true`, it will decode the matched route params using `decodeURIComponent`.
|
|
705
|
-
*
|
|
706
|
-
* @example
|
|
707
|
-
* app.get("/", (event) => {
|
|
708
|
-
* const params = getRouterParams(event); // { key: "value" }
|
|
709
|
-
* });
|
|
710
|
-
*/
|
|
585
|
+
return parseQuery((event.url || new URL(event.req.url)).search.slice(1));
|
|
586
|
+
}
|
|
587
|
+
function getValidatedQuery(event, validate, options) {
|
|
588
|
+
return validateData(getQuery(event), validate, options);
|
|
589
|
+
}
|
|
711
590
|
function getRouterParams(event, opts = {}) {
|
|
712
|
-
|
|
713
|
-
let params = context.params || {};
|
|
591
|
+
let params = getEventContext(event).params || {};
|
|
714
592
|
if (opts.decode) {
|
|
715
593
|
params = { ...params };
|
|
716
594
|
for (const key in params) params[key] = decodeURIComponent(params[key]);
|
|
717
595
|
}
|
|
718
596
|
return params;
|
|
719
597
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
*
|
|
725
|
-
* You can use a simple function to validate the params object or use a Standard-Schema compatible library like `zod` to define a schema.
|
|
726
|
-
*
|
|
727
|
-
* @example
|
|
728
|
-
* app.get("/", async (event) => {
|
|
729
|
-
* const params = await getValidatedRouterParams(event, (data) => {
|
|
730
|
-
* return "key" in data && typeof data.key === "string";
|
|
731
|
-
* });
|
|
732
|
-
* });
|
|
733
|
-
* @example
|
|
734
|
-
* import { z } from "zod";
|
|
735
|
-
*
|
|
736
|
-
* app.get("/", async (event) => {
|
|
737
|
-
* const params = await getValidatedRouterParams(
|
|
738
|
-
* event,
|
|
739
|
-
* z.object({
|
|
740
|
-
* key: z.string(),
|
|
741
|
-
* }),
|
|
742
|
-
* );
|
|
743
|
-
* });
|
|
744
|
-
*/
|
|
745
|
-
function getValidatedRouterParams(event, validate, opts = {}) {
|
|
746
|
-
const routerParams = getRouterParams(event, opts);
|
|
747
|
-
return validateData(routerParams, validate);
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* Get a matched route param by name.
|
|
751
|
-
*
|
|
752
|
-
* If `decode` option is `true`, it will decode the matched route param using `decodeURI`.
|
|
753
|
-
*
|
|
754
|
-
* @example
|
|
755
|
-
* app.get("/", (event) => {
|
|
756
|
-
* const param = getRouterParam(event, "key");
|
|
757
|
-
* });
|
|
758
|
-
*/
|
|
598
|
+
function getValidatedRouterParams(event, validate, options = {}) {
|
|
599
|
+
const { decode, ...opts } = options;
|
|
600
|
+
return validateData(getRouterParams(event, { decode }), validate, opts);
|
|
601
|
+
}
|
|
759
602
|
function getRouterParam(event, name, opts = {}) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
*
|
|
765
|
-
* Checks if the incoming request method is of the expected type.
|
|
766
|
-
*
|
|
767
|
-
* If `allowHead` is `true`, it will allow `HEAD` requests to pass if the expected method is `GET`.
|
|
768
|
-
*
|
|
769
|
-
* @example
|
|
770
|
-
* app.get("/", (event) => {
|
|
771
|
-
* if (isMethod(event, "GET")) {
|
|
772
|
-
* // Handle GET request
|
|
773
|
-
* } else if (isMethod(event, ["POST", "PUT"])) {
|
|
774
|
-
* // Handle POST or PUT request
|
|
775
|
-
* }
|
|
776
|
-
* });
|
|
777
|
-
*/
|
|
603
|
+
return getRouterParams(event, opts)[name];
|
|
604
|
+
}
|
|
778
605
|
function isMethod(event, expected, allowHead) {
|
|
779
606
|
if (allowHead && event.req.method === "HEAD") return true;
|
|
780
607
|
if (typeof expected === "string") {
|
|
@@ -782,75 +609,30 @@ function isMethod(event, expected, allowHead) {
|
|
|
782
609
|
} else if (expected.includes(event.req.method)) return true;
|
|
783
610
|
return false;
|
|
784
611
|
}
|
|
785
|
-
/**
|
|
786
|
-
* Asserts that the incoming request method is of the expected type using `isMethod`.
|
|
787
|
-
*
|
|
788
|
-
* If the method is not allowed, it will throw a 405 error with the message "HTTP method is not allowed".
|
|
789
|
-
*
|
|
790
|
-
* If `allowHead` is `true`, it will allow `HEAD` requests to pass if the expected method is `GET`.
|
|
791
|
-
*
|
|
792
|
-
* @example
|
|
793
|
-
* app.get("/", (event) => {
|
|
794
|
-
* assertMethod(event, "GET");
|
|
795
|
-
* // Handle GET request, otherwise throw 405 error
|
|
796
|
-
* });
|
|
797
|
-
*/
|
|
798
612
|
function assertMethod(event, expected, allowHead) {
|
|
799
|
-
if (!isMethod(event, expected, allowHead))
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
*
|
|
808
|
-
* @example
|
|
809
|
-
* app.get("/", (event) => {
|
|
810
|
-
* const host = getRequestHost(event); // "example.com"
|
|
811
|
-
* });
|
|
812
|
-
*/
|
|
613
|
+
if (!isMethod(event, expected, allowHead)) {
|
|
614
|
+
const allowed = Array.isArray(expected) ? expected : [expected];
|
|
615
|
+
throw new HTTPError({
|
|
616
|
+
status: 405,
|
|
617
|
+
headers: { Allow: allowHead ? [...allowed, "HEAD"].join(", ") : allowed.join(", ") }
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
813
621
|
function getRequestHost(event, opts = {}) {
|
|
814
622
|
if (opts.xForwardedHost) {
|
|
815
|
-
const
|
|
816
|
-
const xForwardedHost = (_header || "").split(",").shift()?.trim();
|
|
623
|
+
const xForwardedHost = (event.req.headers.get("x-forwarded-host") || "").split(",").shift()?.trim();
|
|
817
624
|
if (xForwardedHost) return xForwardedHost;
|
|
818
625
|
}
|
|
819
626
|
return event.req.headers.get("host") || "";
|
|
820
627
|
}
|
|
821
|
-
/**
|
|
822
|
-
* Get the request protocol.
|
|
823
|
-
*
|
|
824
|
-
* If `x-forwarded-proto` header is set to "https", it will return "https". You can disable this behavior by setting `xForwardedProto` to `false`.
|
|
825
|
-
*
|
|
826
|
-
* If protocol cannot be determined, it will default to "http".
|
|
827
|
-
*
|
|
828
|
-
* @example
|
|
829
|
-
* app.get("/", (event) => {
|
|
830
|
-
* const protocol = getRequestProtocol(event); // "https"
|
|
831
|
-
* });
|
|
832
|
-
*/
|
|
833
628
|
function getRequestProtocol(event, opts = {}) {
|
|
834
629
|
if (opts.xForwardedProto !== false) {
|
|
835
630
|
const forwardedProto = event.req.headers.get("x-forwarded-proto");
|
|
836
631
|
if (forwardedProto === "https") return "https";
|
|
837
632
|
if (forwardedProto === "http") return "http";
|
|
838
633
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
/**
|
|
843
|
-
* Generated the full incoming request URL.
|
|
844
|
-
*
|
|
845
|
-
* If `xForwardedHost` is `true`, it will use the `x-forwarded-host` header if it exists.
|
|
846
|
-
*
|
|
847
|
-
* If `xForwardedProto` is `false`, it will not use the `x-forwarded-proto` header.
|
|
848
|
-
*
|
|
849
|
-
* @example
|
|
850
|
-
* app.get("/", (event) => {
|
|
851
|
-
* const url = getRequestURL(event); // "https://example.com/path"
|
|
852
|
-
* });
|
|
853
|
-
*/
|
|
634
|
+
return (event.url || new URL(event.req.url)).protocol.slice(0, -1);
|
|
635
|
+
}
|
|
854
636
|
function getRequestURL(event, opts = {}) {
|
|
855
637
|
const url = new URL(event.url || event.req.url);
|
|
856
638
|
url.protocol = getRequestProtocol(event, opts);
|
|
@@ -858,23 +640,11 @@ function getRequestURL(event, opts = {}) {
|
|
|
858
640
|
const host = getRequestHost(event, opts);
|
|
859
641
|
if (host) {
|
|
860
642
|
url.host = host;
|
|
861
|
-
if (
|
|
643
|
+
if (!/:\d+$/.test(host)) url.port = "";
|
|
862
644
|
}
|
|
863
645
|
}
|
|
864
646
|
return url;
|
|
865
647
|
}
|
|
866
|
-
/**
|
|
867
|
-
* Try to get the client IP address from the incoming request.
|
|
868
|
-
*
|
|
869
|
-
* If `xForwardedFor` is `true`, it will use the `x-forwarded-for` header if it exists.
|
|
870
|
-
*
|
|
871
|
-
* If IP cannot be determined, it will default to `undefined`.
|
|
872
|
-
*
|
|
873
|
-
* @example
|
|
874
|
-
* app.get("/", (event) => {
|
|
875
|
-
* const ip = getRequestIP(event); // "192.0.2.0"
|
|
876
|
-
* });
|
|
877
|
-
*/
|
|
878
648
|
function getRequestIP(event, opts = {}) {
|
|
879
649
|
if (opts.xForwardedFor) {
|
|
880
650
|
const _header = event.req.headers.get("x-forwarded-for");
|
|
@@ -885,9 +655,6 @@ function getRequestIP(event, opts = {}) {
|
|
|
885
655
|
}
|
|
886
656
|
return event.req.context?.clientAddress || event.req.ip || void 0;
|
|
887
657
|
}
|
|
888
|
-
|
|
889
|
-
//#endregion
|
|
890
|
-
//#region src/handler.ts
|
|
891
658
|
function defineHandler(input) {
|
|
892
659
|
if (typeof input === "function") return handlerWithFetch(input);
|
|
893
660
|
const handler = input.handler || (input.fetch ? function _fetchHandler(event) {
|
|
@@ -897,9 +664,6 @@ function defineHandler(input) {
|
|
|
897
664
|
return callMiddleware(event, input.middleware, handler);
|
|
898
665
|
} : handler), input);
|
|
899
666
|
}
|
|
900
|
-
/**
|
|
901
|
-
* @experimental defineValidatedHandler is an experimental feature and API may change.
|
|
902
|
-
*/
|
|
903
667
|
function defineValidatedHandler(def) {
|
|
904
668
|
if (!def.validate) return defineHandler(def);
|
|
905
669
|
return defineHandler({
|
|
@@ -935,16 +699,12 @@ function dynamicEventHandler(initial) {
|
|
|
935
699
|
function defineLazyEventHandler(loader) {
|
|
936
700
|
let handler;
|
|
937
701
|
let promise;
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return promise ??= Promise.resolve(loader()).then((r) => {
|
|
702
|
+
return defineHandler(function lazyHandler(event) {
|
|
703
|
+
return handler ? handler(event) : (promise ??= Promise.resolve(loader()).then(function resolveLazyHandler(r) {
|
|
941
704
|
handler = toEventHandler(r) || toEventHandler(r.default);
|
|
942
705
|
if (typeof handler !== "function") throw new TypeError("Invalid lazy handler", { cause: { resolved: r } });
|
|
943
706
|
return handler;
|
|
944
|
-
});
|
|
945
|
-
};
|
|
946
|
-
return defineHandler(function lazyHandler(event) {
|
|
947
|
-
return handler ? handler(event) : resolveLazyHandler().then((r) => r(event));
|
|
707
|
+
})).then((r) => r(event));
|
|
948
708
|
});
|
|
949
709
|
}
|
|
950
710
|
function toEventHandler(handler) {
|
|
@@ -954,103 +714,96 @@ function toEventHandler(handler) {
|
|
|
954
714
|
return handler.fetch(event.req);
|
|
955
715
|
};
|
|
956
716
|
}
|
|
957
|
-
|
|
958
|
-
//#endregion
|
|
959
|
-
//#region src/h3.ts
|
|
960
717
|
const NoHandler = () => kNotFound;
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
"
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
718
|
+
var H3Core = class {
|
|
719
|
+
config;
|
|
720
|
+
"~middleware";
|
|
721
|
+
"~routes" = [];
|
|
722
|
+
constructor(config = {}) {
|
|
723
|
+
this["~middleware"] = [];
|
|
724
|
+
this.config = config;
|
|
725
|
+
this.fetch = this.fetch.bind(this);
|
|
726
|
+
this.handler = this.handler.bind(this);
|
|
727
|
+
}
|
|
728
|
+
fetch(request) {
|
|
729
|
+
return this["~request"](request);
|
|
730
|
+
}
|
|
731
|
+
handler(event) {
|
|
732
|
+
const route = this["~findRoute"](event);
|
|
733
|
+
if (route) {
|
|
734
|
+
event.context.params = route.params;
|
|
735
|
+
event.context.matchedRoute = route.data;
|
|
736
|
+
}
|
|
737
|
+
const routeHandler = route?.data.handler || NoHandler;
|
|
738
|
+
const middleware = this["~getMiddleware"](event, route);
|
|
739
|
+
return middleware.length > 0 ? callMiddleware(event, middleware, routeHandler) : routeHandler(event);
|
|
740
|
+
}
|
|
741
|
+
"~request"(request, context) {
|
|
742
|
+
const event = new H3Event(request, context, this);
|
|
743
|
+
let handlerRes;
|
|
744
|
+
try {
|
|
745
|
+
if (this.config.onRequest) {
|
|
746
|
+
const hookRes = this.config.onRequest(event);
|
|
747
|
+
handlerRes = typeof hookRes?.then === "function" ? hookRes.then(() => this.handler(event)) : this.handler(event);
|
|
748
|
+
} else handlerRes = this.handler(event);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
handlerRes = Promise.reject(error);
|
|
751
|
+
}
|
|
752
|
+
return toResponse(handlerRes, event, this.config);
|
|
753
|
+
}
|
|
754
|
+
"~findRoute"(_event) {}
|
|
755
|
+
"~addRoute"(_route) {
|
|
756
|
+
this["~routes"].push(_route);
|
|
757
|
+
}
|
|
758
|
+
"~getMiddleware"(_event, route) {
|
|
759
|
+
const routeMiddleware = route?.data.middleware;
|
|
760
|
+
const globalMiddleware = this["~middleware"];
|
|
761
|
+
return routeMiddleware ? [...globalMiddleware, ...routeMiddleware] : globalMiddleware;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
const H3 = /* @__PURE__ */ (() => {
|
|
765
|
+
class H3 extends H3Core {
|
|
766
|
+
"~rou3";
|
|
977
767
|
constructor(config = {}) {
|
|
978
|
-
|
|
979
|
-
this
|
|
980
|
-
this.fetch = this.fetch.bind(this);
|
|
768
|
+
super(config);
|
|
769
|
+
this["~rou3"] = createRouter();
|
|
981
770
|
this.request = this.request.bind(this);
|
|
982
|
-
this.handler = this.handler.bind(this);
|
|
983
771
|
config.plugins?.forEach((plugin) => plugin(this));
|
|
984
772
|
}
|
|
985
|
-
fetch(request) {
|
|
986
|
-
return this._request(request);
|
|
987
|
-
}
|
|
988
|
-
request(_req, _init, context) {
|
|
989
|
-
return this._request(toRequest(_req, _init), context);
|
|
990
|
-
}
|
|
991
|
-
_request(request, context) {
|
|
992
|
-
const event = new H3Event(request, context, this);
|
|
993
|
-
let handlerRes;
|
|
994
|
-
try {
|
|
995
|
-
if (this.config.onRequest) {
|
|
996
|
-
const hookRes = this.config.onRequest(event);
|
|
997
|
-
handlerRes = typeof hookRes?.then === "function" ? hookRes.then(() => this.handler(event)) : this.handler(event);
|
|
998
|
-
} else handlerRes = this.handler(event);
|
|
999
|
-
} catch (error) {
|
|
1000
|
-
handlerRes = Promise.reject(error);
|
|
1001
|
-
}
|
|
1002
|
-
return toResponse(handlerRes, event, this.config);
|
|
1003
|
-
}
|
|
1004
|
-
/**
|
|
1005
|
-
* Immediately register an H3 plugin.
|
|
1006
|
-
*/
|
|
1007
773
|
register(plugin) {
|
|
1008
774
|
plugin(this);
|
|
1009
775
|
return this;
|
|
1010
776
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
this._routes.push(_route);
|
|
1014
|
-
}
|
|
1015
|
-
_getMiddleware(_event, route) {
|
|
1016
|
-
return route?.data.middleware ? [...this._middleware, ...route.data.middleware] : this._middleware;
|
|
1017
|
-
}
|
|
1018
|
-
handler(event) {
|
|
1019
|
-
const route = this._findRoute(event);
|
|
1020
|
-
if (route) {
|
|
1021
|
-
event.context.params = route.params;
|
|
1022
|
-
event.context.matchedRoute = route.data;
|
|
1023
|
-
}
|
|
1024
|
-
const routeHandler = route?.data.handler || NoHandler;
|
|
1025
|
-
const middleware = this._getMiddleware(event, route);
|
|
1026
|
-
return middleware.length > 0 ? callMiddleware(event, middleware, routeHandler) : routeHandler(event);
|
|
777
|
+
request(_req, _init, context) {
|
|
778
|
+
return this["~request"](toRequest(_req, _init), context);
|
|
1027
779
|
}
|
|
1028
780
|
mount(base, input) {
|
|
1029
781
|
if ("handler" in input) {
|
|
1030
|
-
if (input.
|
|
1031
|
-
|
|
782
|
+
if (input["~middleware"].length > 0) this["~middleware"].push((event, next) => {
|
|
783
|
+
const originalPathname = event.url.pathname;
|
|
784
|
+
if (!originalPathname.startsWith(base) || originalPathname.length > base.length && originalPathname[base.length] !== "/") return next();
|
|
785
|
+
event.url.pathname = event.url.pathname.slice(base.length) || "/";
|
|
786
|
+
return callMiddleware(event, input["~middleware"], () => {
|
|
787
|
+
event.url.pathname = originalPathname;
|
|
788
|
+
return next();
|
|
789
|
+
});
|
|
1032
790
|
});
|
|
1033
|
-
for (const r of input
|
|
791
|
+
for (const r of input["~routes"]) this["~addRoute"]({
|
|
1034
792
|
...r,
|
|
1035
793
|
route: base + r.route
|
|
1036
794
|
});
|
|
1037
795
|
} else {
|
|
1038
796
|
const fetchHandler = "fetch" in input ? input.fetch : input;
|
|
1039
797
|
this.all(`${base}/**`, function _mountedMiddleware(event) {
|
|
1040
|
-
|
|
1041
|
-
url.pathname = url.pathname.slice(base.length) || "/";
|
|
1042
|
-
return fetchHandler(new Request(url, event.req));
|
|
798
|
+
return fetchHandler(requestWithBaseURL(event.req, base));
|
|
1043
799
|
});
|
|
1044
800
|
}
|
|
1045
801
|
return this;
|
|
1046
802
|
}
|
|
1047
|
-
all(route, handler, opts) {
|
|
1048
|
-
return this.on("", route, handler, opts);
|
|
1049
|
-
}
|
|
1050
803
|
on(method, route, handler, opts) {
|
|
1051
804
|
const _method = (method || "").toUpperCase();
|
|
1052
805
|
route = new URL(route, "http://_").pathname;
|
|
1053
|
-
this
|
|
806
|
+
this["~addRoute"]({
|
|
1054
807
|
method: _method,
|
|
1055
808
|
route,
|
|
1056
809
|
handler: toEventHandler(handler),
|
|
@@ -1062,8 +815,15 @@ const H3Core = /* @__PURE__ */ (() => {
|
|
|
1062
815
|
});
|
|
1063
816
|
return this;
|
|
1064
817
|
}
|
|
1065
|
-
|
|
1066
|
-
return
|
|
818
|
+
all(route, handler, opts) {
|
|
819
|
+
return this.on("", route, handler, opts);
|
|
820
|
+
}
|
|
821
|
+
"~findRoute"(_event) {
|
|
822
|
+
return findRoute(this["~rou3"], _event.req.method, _event.url.pathname);
|
|
823
|
+
}
|
|
824
|
+
"~addRoute"(_route) {
|
|
825
|
+
addRoute(this["~rou3"], _route.method, _route.route, _route);
|
|
826
|
+
super["~addRoute"](_route);
|
|
1067
827
|
}
|
|
1068
828
|
use(arg1, arg2, arg3) {
|
|
1069
829
|
let route;
|
|
@@ -1077,42 +837,29 @@ const H3Core = /* @__PURE__ */ (() => {
|
|
|
1077
837
|
fn = arg1;
|
|
1078
838
|
opts = arg2;
|
|
1079
839
|
}
|
|
1080
|
-
|
|
840
|
+
if (typeof fn !== "function" && "handler" in fn) return this.mount(route || "", fn);
|
|
841
|
+
this["~middleware"].push(normalizeMiddleware(fn, {
|
|
1081
842
|
...opts,
|
|
1082
843
|
route
|
|
1083
844
|
}));
|
|
1084
845
|
return this;
|
|
1085
846
|
}
|
|
1086
847
|
}
|
|
1087
|
-
for (const method of
|
|
848
|
+
for (const method of [
|
|
849
|
+
"GET",
|
|
850
|
+
"POST",
|
|
851
|
+
"PUT",
|
|
852
|
+
"DELETE",
|
|
853
|
+
"PATCH",
|
|
854
|
+
"HEAD",
|
|
855
|
+
"OPTIONS",
|
|
856
|
+
"CONNECT",
|
|
857
|
+
"TRACE"
|
|
858
|
+
]) H3Core.prototype[method.toLowerCase()] = function(route, handler, opts) {
|
|
1088
859
|
return this.on(method, route, handler, opts);
|
|
1089
860
|
};
|
|
1090
|
-
return
|
|
861
|
+
return H3;
|
|
1091
862
|
})();
|
|
1092
|
-
var H3 = class extends H3Core {
|
|
1093
|
-
/** @internal */
|
|
1094
|
-
_rou3;
|
|
1095
|
-
constructor(config = {}) {
|
|
1096
|
-
super(config);
|
|
1097
|
-
this._rou3 = createRouter();
|
|
1098
|
-
}
|
|
1099
|
-
_findRoute(_event) {
|
|
1100
|
-
return findRoute(this._rou3, _event.req.method, _event.url.pathname);
|
|
1101
|
-
}
|
|
1102
|
-
_addRoute(_route) {
|
|
1103
|
-
addRoute(this._rou3, _route.method, _route.route, _route);
|
|
1104
|
-
super._addRoute(_route);
|
|
1105
|
-
}
|
|
1106
|
-
_normalizeMiddleware(fn, opts) {
|
|
1107
|
-
return normalizeMiddleware(fn, opts);
|
|
1108
|
-
}
|
|
1109
|
-
};
|
|
1110
|
-
|
|
1111
|
-
//#endregion
|
|
1112
|
-
//#region src/adapters.ts
|
|
1113
|
-
/**
|
|
1114
|
-
* @deprecated Since h3 v2 you can directly use `app.fetch(request, init?, context?)`
|
|
1115
|
-
*/
|
|
1116
863
|
function toWebHandler(app) {
|
|
1117
864
|
return (request, context) => {
|
|
1118
865
|
return Promise.resolve(app.request(request, void 0, context || request.context));
|
|
@@ -1141,8 +888,16 @@ function callNodeHandler(handler, req, res) {
|
|
|
1141
888
|
return new Promise((resolve, reject) => {
|
|
1142
889
|
res.once("close", () => resolve(kHandled));
|
|
1143
890
|
res.once("finish", () => resolve(kHandled));
|
|
1144
|
-
res.once("pipe", (stream) => resolve(stream));
|
|
1145
891
|
res.once("error", (error) => reject(error));
|
|
892
|
+
res.once("pipe", (stream) => {
|
|
893
|
+
resolve(new Promise((resolve, reject) => {
|
|
894
|
+
stream.once("close", () => resolve(kHandled));
|
|
895
|
+
stream.once("error", (error) => {
|
|
896
|
+
console.error("[h3] Stream error in Node.js handler", { cause: error });
|
|
897
|
+
reject(kHandled);
|
|
898
|
+
});
|
|
899
|
+
}));
|
|
900
|
+
});
|
|
1146
901
|
try {
|
|
1147
902
|
if (isMiddleware) Promise.resolve(handler(req, res, (error) => error ? reject(new HTTPError({
|
|
1148
903
|
cause: error,
|
|
@@ -1163,44 +918,19 @@ function callNodeHandler(handler, req, res) {
|
|
|
1163
918
|
}
|
|
1164
919
|
});
|
|
1165
920
|
}
|
|
1166
|
-
|
|
1167
|
-
//#endregion
|
|
1168
|
-
//#region src/utils/route.ts
|
|
1169
|
-
/**
|
|
1170
|
-
* Define a route as a plugin that can be registered with app.register()
|
|
1171
|
-
*
|
|
1172
|
-
* @example
|
|
1173
|
-
* ```js
|
|
1174
|
-
* import { z } from "zod";
|
|
1175
|
-
*
|
|
1176
|
-
* const userRoute = defineRoute({
|
|
1177
|
-
* method: 'POST',
|
|
1178
|
-
* validate: {
|
|
1179
|
-
* query: z.object({ id: z.string().uuid() }),
|
|
1180
|
-
* body: z.object({ name: z.string() }),
|
|
1181
|
-
* },
|
|
1182
|
-
* handler: (event) => {
|
|
1183
|
-
* return { success: true };
|
|
1184
|
-
* }
|
|
1185
|
-
* });
|
|
1186
|
-
*
|
|
1187
|
-
* app.register(userRoute);
|
|
1188
|
-
* ```
|
|
1189
|
-
*/
|
|
1190
921
|
function defineRoute(def) {
|
|
1191
922
|
const handler = defineValidatedHandler(def);
|
|
1192
923
|
return (h3) => {
|
|
1193
924
|
h3.on(def.method, def.route, handler);
|
|
1194
925
|
};
|
|
1195
926
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
*/
|
|
927
|
+
function removeRoute$1(app, method, route) {
|
|
928
|
+
const _method = method ? method.toUpperCase() : void 0;
|
|
929
|
+
route = new URL(route, "http://_").pathname;
|
|
930
|
+
removeRoute(app["~rou3"], _method || "", route);
|
|
931
|
+
const idx = app["~routes"].findIndex((r) => r.route === route && (_method == null || r.method === _method));
|
|
932
|
+
if (idx !== -1) app["~routes"].splice(idx, 1);
|
|
933
|
+
}
|
|
1204
934
|
const textEncoder = /* @__PURE__ */ new TextEncoder();
|
|
1205
935
|
const textDecoder = /* @__PURE__ */ new TextDecoder();
|
|
1206
936
|
const base64Code = [
|
|
@@ -1295,19 +1025,6 @@ function validateBinaryLike(source) {
|
|
|
1295
1025
|
else if (source instanceof ArrayBuffer) return new Uint8Array(source);
|
|
1296
1026
|
throw new TypeError(`The input must be a Uint8Array, a string, or an ArrayBuffer.`);
|
|
1297
1027
|
}
|
|
1298
|
-
|
|
1299
|
-
//#endregion
|
|
1300
|
-
//#region src/utils/internal/iterable.ts
|
|
1301
|
-
/**
|
|
1302
|
-
* The default implementation for {@link iterable}'s `serializer` argument.
|
|
1303
|
-
* It serializes values as follows:
|
|
1304
|
-
* - Instances of {@link String}, {@link Uint8Array} and `undefined` are returned as-is.
|
|
1305
|
-
* - Objects are serialized through {@link JSON.stringify}.
|
|
1306
|
-
* - Functions are serialized as `undefined`.
|
|
1307
|
-
* - Values of type boolean, number, bigint or symbol are serialized using their `toString` function.
|
|
1308
|
-
*
|
|
1309
|
-
* @param value - The value to serialize to either a string or Uint8Array.
|
|
1310
|
-
*/
|
|
1311
1028
|
function serializeIterableValue(value) {
|
|
1312
1029
|
switch (typeof value) {
|
|
1313
1030
|
case "string": return textEncoder.encode(value);
|
|
@@ -1315,57 +1032,31 @@ function serializeIterableValue(value) {
|
|
|
1315
1032
|
case "number":
|
|
1316
1033
|
case "bigint":
|
|
1317
1034
|
case "symbol": return textEncoder.encode(value.toString());
|
|
1318
|
-
case "object":
|
|
1035
|
+
case "object":
|
|
1319
1036
|
if (value instanceof Uint8Array) return value;
|
|
1320
1037
|
return textEncoder.encode(JSON.stringify(value));
|
|
1321
|
-
}
|
|
1322
1038
|
}
|
|
1323
1039
|
return new Uint8Array();
|
|
1324
1040
|
}
|
|
1325
|
-
function coerceIterable(iterable
|
|
1326
|
-
if (typeof iterable
|
|
1327
|
-
if (Symbol.iterator in iterable
|
|
1328
|
-
if (Symbol.asyncIterator in iterable
|
|
1329
|
-
return iterable
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
//#endregion
|
|
1333
|
-
//#region src/utils/response.ts
|
|
1334
|
-
/**
|
|
1335
|
-
* Respond with an empty payload.<br>
|
|
1336
|
-
*
|
|
1337
|
-
* @example
|
|
1338
|
-
* app.get("/", () => noContent());
|
|
1339
|
-
*
|
|
1340
|
-
* @param status status code to be send. By default, it is `204 No Content`.
|
|
1341
|
-
*/
|
|
1041
|
+
function coerceIterable(iterable) {
|
|
1042
|
+
if (typeof iterable === "function") iterable = iterable();
|
|
1043
|
+
if (Symbol.iterator in iterable) return iterable[Symbol.iterator]();
|
|
1044
|
+
if (Symbol.asyncIterator in iterable) return iterable[Symbol.asyncIterator]();
|
|
1045
|
+
return iterable;
|
|
1046
|
+
}
|
|
1342
1047
|
function noContent(status = 204) {
|
|
1343
1048
|
return new HTTPResponse(null, {
|
|
1344
1049
|
status,
|
|
1345
1050
|
statusText: "No Content"
|
|
1346
1051
|
});
|
|
1347
1052
|
}
|
|
1348
|
-
/**
|
|
1349
|
-
* Send a redirect response to the client.
|
|
1350
|
-
*
|
|
1351
|
-
* It adds the `location` header to the response and sets the status code to 302 by default.
|
|
1352
|
-
*
|
|
1353
|
-
* In the body, it sends a simple HTML page with a meta refresh tag to redirect the client in case the headers are ignored.
|
|
1354
|
-
*
|
|
1355
|
-
* @example
|
|
1356
|
-
* app.get("/", () => {
|
|
1357
|
-
* return redirect("https://example.com");
|
|
1358
|
-
* });
|
|
1359
|
-
*
|
|
1360
|
-
* @example
|
|
1361
|
-
* app.get("/", () => {
|
|
1362
|
-
* return redirect("https://example.com", 301); // Permanent redirect
|
|
1363
|
-
* });
|
|
1364
|
-
*/
|
|
1365
1053
|
function redirect(location, status = 302, statusText) {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1054
|
+
return new HTTPResponse(`<html><head><meta http-equiv="refresh" content="0; url=${location.replace(/[&"<>]/g, (c) => ({
|
|
1055
|
+
"&": "&",
|
|
1056
|
+
"\"": """,
|
|
1057
|
+
"<": "<",
|
|
1058
|
+
">": ">"
|
|
1059
|
+
})[c])}" /></head></html>`, {
|
|
1369
1060
|
status,
|
|
1370
1061
|
statusText: statusText || (status === 301 ? "Moved Permanently" : "Found"),
|
|
1371
1062
|
headers: {
|
|
@@ -1374,49 +1065,32 @@ function redirect(location, status = 302, statusText) {
|
|
|
1374
1065
|
}
|
|
1375
1066
|
});
|
|
1376
1067
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1068
|
+
function redirectBack(event, opts = {}) {
|
|
1069
|
+
const referer = event.req.headers.get("referer");
|
|
1070
|
+
let location = opts.fallback ?? "/";
|
|
1071
|
+
if (referer && URL.canParse(referer)) {
|
|
1072
|
+
const refererURL = new URL(referer);
|
|
1073
|
+
if (refererURL.origin === event.url.origin) {
|
|
1074
|
+
let pathname = refererURL.pathname;
|
|
1075
|
+
if (pathname.startsWith("//")) pathname = "/" + pathname.replace(/^\/+/, "");
|
|
1076
|
+
location = pathname + (opts.allowQuery ? refererURL.search : "");
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return redirect(location, opts.status);
|
|
1080
|
+
}
|
|
1380
1081
|
function writeEarlyHints(event, hints) {
|
|
1381
|
-
if (
|
|
1382
|
-
return new Promise((resolve) => {
|
|
1082
|
+
if (event.runtime?.node?.res?.writeEarlyHints) return new Promise((resolve) => {
|
|
1383
1083
|
event.runtime?.node?.res?.writeEarlyHints(hints, () => resolve());
|
|
1384
1084
|
});
|
|
1085
|
+
for (const [name, value] of Object.entries(hints)) {
|
|
1086
|
+
if (name.toLowerCase() !== "link") continue;
|
|
1087
|
+
if (Array.isArray(value)) for (const v of value) event.res.headers.append("link", v);
|
|
1088
|
+
else event.res.headers.append("link", value);
|
|
1089
|
+
}
|
|
1385
1090
|
}
|
|
1386
|
-
|
|
1387
|
-
* Iterate a source of chunks and send back each chunk in order.
|
|
1388
|
-
* Supports mixing async work together with emitting chunks.
|
|
1389
|
-
*
|
|
1390
|
-
* Each chunk must be a string or a buffer.
|
|
1391
|
-
*
|
|
1392
|
-
* For generator (yielding) functions, the returned value is treated the same as yielded values.
|
|
1393
|
-
*
|
|
1394
|
-
* @param iterable - Iterator that produces chunks of the response.
|
|
1395
|
-
* @param serializer - Function that converts values from the iterable into stream-compatible values.
|
|
1396
|
-
* @template Value - Test
|
|
1397
|
-
*
|
|
1398
|
-
* @example
|
|
1399
|
-
* return iterable(async function* work() {
|
|
1400
|
-
* // Open document body
|
|
1401
|
-
* yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n";
|
|
1402
|
-
* // Do work ...
|
|
1403
|
-
* for (let i = 0; i < 1000) {
|
|
1404
|
-
* await delay(1000);
|
|
1405
|
-
* // Report progress
|
|
1406
|
-
* yield `<li>Completed job #`;
|
|
1407
|
-
* yield i;
|
|
1408
|
-
* yield `</li>\n`;
|
|
1409
|
-
* }
|
|
1410
|
-
* // Close out the report
|
|
1411
|
-
* return `</ol></body></html>`;
|
|
1412
|
-
* })
|
|
1413
|
-
* async function delay(ms) {
|
|
1414
|
-
* return new Promise(resolve => setTimeout(resolve, ms));
|
|
1415
|
-
* }
|
|
1416
|
-
*/
|
|
1417
|
-
function iterable(iterable$1, options) {
|
|
1091
|
+
function iterable(iterable, options) {
|
|
1418
1092
|
const serializer = options?.serializer ?? serializeIterableValue;
|
|
1419
|
-
const iterator = coerceIterable(iterable
|
|
1093
|
+
const iterator = coerceIterable(iterable);
|
|
1420
1094
|
return new HTTPResponse(new ReadableStream({
|
|
1421
1095
|
async pull(controller) {
|
|
1422
1096
|
const { value, done } = await iterator.next();
|
|
@@ -1432,38 +1106,73 @@ function iterable(iterable$1, options) {
|
|
|
1432
1106
|
}));
|
|
1433
1107
|
}
|
|
1434
1108
|
function html(first, ...values) {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1109
|
+
return new HTTPResponse(typeof first === "string" ? first : first.reduce((out, str, i) => out + str + (values[i] ?? ""), ""), { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
1110
|
+
}
|
|
1111
|
+
function parseURLEncodedBody(body) {
|
|
1112
|
+
const form = new URLSearchParams(body);
|
|
1113
|
+
const parsedForm = new EmptyObject();
|
|
1114
|
+
for (const [key, value] of form.entries()) if (hasProp(parsedForm, key)) {
|
|
1115
|
+
if (!Array.isArray(parsedForm[key])) parsedForm[key] = [parsedForm[key]];
|
|
1116
|
+
parsedForm[key].push(value);
|
|
1117
|
+
} else parsedForm[key] = value;
|
|
1118
|
+
return parsedForm;
|
|
1119
|
+
}
|
|
1120
|
+
async function readBody(event) {
|
|
1121
|
+
const text = await event.req.text();
|
|
1122
|
+
if (!text) return;
|
|
1123
|
+
if ((event.req.headers.get("content-type") || "").startsWith("application/x-www-form-urlencoded")) return parseURLEncodedBody(text);
|
|
1124
|
+
try {
|
|
1125
|
+
return JSON.parse(text);
|
|
1126
|
+
} catch {
|
|
1127
|
+
throw new HTTPError({
|
|
1128
|
+
status: 400,
|
|
1129
|
+
statusText: "Bad Request",
|
|
1130
|
+
message: "Invalid JSON body"
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
async function readValidatedBody(event, validate, options) {
|
|
1135
|
+
return validateData(await readBody(event), validate, options);
|
|
1136
|
+
}
|
|
1137
|
+
async function assertBodySize(event, limit) {
|
|
1138
|
+
if (!await isBodySizeWithin(event, limit)) throw new HTTPError({
|
|
1139
|
+
status: 413,
|
|
1140
|
+
statusText: "Request Entity Too Large",
|
|
1141
|
+
message: `Request body size exceeds the limit of ${limit} bytes`
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
async function isBodySizeWithin(event, limit) {
|
|
1145
|
+
const req = event.req;
|
|
1146
|
+
if (req.body === null) return true;
|
|
1147
|
+
const contentLength = req.headers.get("content-length");
|
|
1148
|
+
if (contentLength) {
|
|
1149
|
+
if (req.headers.get("transfer-encoding")) throw new HTTPError({ status: 400 });
|
|
1150
|
+
if (+contentLength > limit) return false;
|
|
1151
|
+
}
|
|
1152
|
+
const reader = req.clone().body.getReader();
|
|
1153
|
+
let chunk = await reader.read();
|
|
1154
|
+
let size = 0;
|
|
1155
|
+
while (!chunk.done) {
|
|
1156
|
+
size += chunk.value.byteLength;
|
|
1157
|
+
if (size > limit) {
|
|
1158
|
+
reader.cancel();
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
chunk = await reader.read();
|
|
1162
|
+
}
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1444
1165
|
function onRequest(hook) {
|
|
1445
1166
|
return async function _onRequestMiddleware(event) {
|
|
1446
1167
|
await hook(event);
|
|
1447
1168
|
};
|
|
1448
1169
|
}
|
|
1449
|
-
/**
|
|
1450
|
-
* Define a middleware that runs after Response is generated.
|
|
1451
|
-
*
|
|
1452
|
-
* You can return a new Response from the handler to replace the original response.
|
|
1453
|
-
*/
|
|
1454
1170
|
function onResponse(hook) {
|
|
1455
1171
|
return async function _onResponseMiddleware(event, next) {
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
const hookResponse = await hook(response, event);
|
|
1459
|
-
return hookResponse || response;
|
|
1172
|
+
const response = await toResponse(await next(), event);
|
|
1173
|
+
return await hook(response, event) || response;
|
|
1460
1174
|
};
|
|
1461
1175
|
}
|
|
1462
|
-
/**
|
|
1463
|
-
* Define a middleware that runs when an error occurs.
|
|
1464
|
-
*
|
|
1465
|
-
* You can return a new Response from the handler to gracefully handle the error.
|
|
1466
|
-
*/
|
|
1467
1176
|
function onError(hook) {
|
|
1468
1177
|
return async (event, next) => {
|
|
1469
1178
|
try {
|
|
@@ -1481,9 +1190,12 @@ function onError(hook) {
|
|
|
1481
1190
|
}
|
|
1482
1191
|
};
|
|
1483
1192
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1193
|
+
function bodyLimit(limit) {
|
|
1194
|
+
return async (event, next) => {
|
|
1195
|
+
await assertBodySize(event, limit);
|
|
1196
|
+
return next();
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1487
1199
|
const PayloadMethods = new Set([
|
|
1488
1200
|
"PATCH",
|
|
1489
1201
|
"POST",
|
|
@@ -1509,22 +1221,16 @@ function rewriteCookieProperty(header, map, property) {
|
|
|
1509
1221
|
return newValue ? prefix + newValue : "";
|
|
1510
1222
|
});
|
|
1511
1223
|
}
|
|
1512
|
-
function mergeHeaders(defaults
|
|
1224
|
+
function mergeHeaders(defaults, ...inputs) {
|
|
1513
1225
|
const _inputs = inputs.filter(Boolean);
|
|
1514
|
-
if (_inputs.length === 0) return defaults
|
|
1515
|
-
const merged = new Headers(defaults
|
|
1226
|
+
if (_inputs.length === 0) return defaults;
|
|
1227
|
+
const merged = new Headers(defaults);
|
|
1516
1228
|
for (const input of _inputs) {
|
|
1517
1229
|
const entries = Array.isArray(input) ? input : typeof input.entries === "function" ? input.entries() : Object.entries(input);
|
|
1518
1230
|
for (const [key, value] of entries) if (value !== void 0) merged.set(key, value);
|
|
1519
1231
|
}
|
|
1520
1232
|
return merged;
|
|
1521
1233
|
}
|
|
1522
|
-
|
|
1523
|
-
//#endregion
|
|
1524
|
-
//#region src/utils/proxy.ts
|
|
1525
|
-
/**
|
|
1526
|
-
* Proxy the incoming request to a target URL.
|
|
1527
|
-
*/
|
|
1528
1234
|
async function proxyRequest(event, target, opts = {}) {
|
|
1529
1235
|
const requestBody = PayloadMethods.has(event.req.method) ? event.req.body : void 0;
|
|
1530
1236
|
const method = opts.fetchOptions?.method || event.req.method;
|
|
@@ -1544,9 +1250,6 @@ async function proxyRequest(event, target, opts = {}) {
|
|
|
1544
1250
|
}
|
|
1545
1251
|
});
|
|
1546
1252
|
}
|
|
1547
|
-
/**
|
|
1548
|
-
* Make a proxy request to a target URL and send the response back to the client.
|
|
1549
|
-
*/
|
|
1550
1253
|
async function proxy(event, target, opts = {}) {
|
|
1551
1254
|
const fetchOptions = {
|
|
1552
1255
|
headers: opts.headers,
|
|
@@ -1564,13 +1267,12 @@ async function proxy(event, target, opts = {}) {
|
|
|
1564
1267
|
const headers = new Headers();
|
|
1565
1268
|
const cookies = [];
|
|
1566
1269
|
for (const [key, value] of response.headers.entries()) {
|
|
1567
|
-
if (key === "content-encoding") continue;
|
|
1568
|
-
if (key === "content-length") continue;
|
|
1270
|
+
if (key === "content-encoding" || key === "content-length" || key === "transfer-encoding") continue;
|
|
1569
1271
|
if (key === "set-cookie") {
|
|
1570
|
-
cookies.push(
|
|
1272
|
+
cookies.push(value);
|
|
1571
1273
|
continue;
|
|
1572
1274
|
}
|
|
1573
|
-
headers.
|
|
1275
|
+
headers.append(key, value);
|
|
1574
1276
|
}
|
|
1575
1277
|
if (cookies.length > 0) {
|
|
1576
1278
|
const _cookies = cookies.map((cookie) => {
|
|
@@ -1587,9 +1289,6 @@ async function proxy(event, target, opts = {}) {
|
|
|
1587
1289
|
headers
|
|
1588
1290
|
});
|
|
1589
1291
|
}
|
|
1590
|
-
/**
|
|
1591
|
-
* Get the request headers object without headers known to cause issues when proxying.
|
|
1592
|
-
*/
|
|
1593
1292
|
function getProxyRequestHeaders(event, opts) {
|
|
1594
1293
|
const headers = new EmptyObject();
|
|
1595
1294
|
for (const [name, value] of event.req.headers.entries()) {
|
|
@@ -1605,9 +1304,6 @@ function getProxyRequestHeaders(event, opts) {
|
|
|
1605
1304
|
}
|
|
1606
1305
|
return headers;
|
|
1607
1306
|
}
|
|
1608
|
-
/**
|
|
1609
|
-
* Make a fetch request with the event's context and headers.
|
|
1610
|
-
*/
|
|
1611
1307
|
async function fetchWithEvent(event, url, init) {
|
|
1612
1308
|
if (url[0] !== "/") return fetch(url, init);
|
|
1613
1309
|
return event.app.fetch(createSubRequest(event, url, {
|
|
@@ -1623,123 +1319,281 @@ function createSubRequest(event, path, init) {
|
|
|
1623
1319
|
req.ip = event.req.ip;
|
|
1624
1320
|
return req;
|
|
1625
1321
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
return
|
|
1322
|
+
const COOKIE_MAX_AGE_LIMIT = 3456e4;
|
|
1323
|
+
function endIndex(str, min, len) {
|
|
1324
|
+
const index = str.indexOf(";", min);
|
|
1325
|
+
return index === -1 ? len : index;
|
|
1326
|
+
}
|
|
1327
|
+
function eqIndex(str, min, max) {
|
|
1328
|
+
const index = str.indexOf("=", min);
|
|
1329
|
+
return index < max ? index : -1;
|
|
1330
|
+
}
|
|
1331
|
+
function valueSlice(str, min, max) {
|
|
1332
|
+
if (min === max) return "";
|
|
1333
|
+
let start = min;
|
|
1334
|
+
let end = max;
|
|
1335
|
+
do {
|
|
1336
|
+
const code = str.charCodeAt(start);
|
|
1337
|
+
if (code !== 32 && code !== 9) break;
|
|
1338
|
+
} while (++start < end);
|
|
1339
|
+
while (end > start) {
|
|
1340
|
+
const code = str.charCodeAt(end - 1);
|
|
1341
|
+
if (code !== 32 && code !== 9) break;
|
|
1342
|
+
end--;
|
|
1343
|
+
}
|
|
1344
|
+
return str.slice(start, end);
|
|
1345
|
+
}
|
|
1346
|
+
const NullObject = /* @__PURE__ */ (() => {
|
|
1347
|
+
const C = function() {};
|
|
1348
|
+
C.prototype = Object.create(null);
|
|
1349
|
+
return C;
|
|
1350
|
+
})();
|
|
1351
|
+
function parse(str, options) {
|
|
1352
|
+
const obj = new NullObject();
|
|
1353
|
+
const len = str.length;
|
|
1354
|
+
if (len < 2) return obj;
|
|
1355
|
+
const dec = options?.decode || decode;
|
|
1356
|
+
const allowMultiple = options?.allowMultiple || false;
|
|
1357
|
+
let index = 0;
|
|
1358
|
+
do {
|
|
1359
|
+
const eqIdx = eqIndex(str, index, len);
|
|
1360
|
+
if (eqIdx === -1) break;
|
|
1361
|
+
const endIdx = endIndex(str, index, len);
|
|
1362
|
+
if (eqIdx > endIdx) {
|
|
1363
|
+
index = str.lastIndexOf(";", eqIdx - 1) + 1;
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
const key = valueSlice(str, index, eqIdx);
|
|
1367
|
+
if (options?.filter && !options.filter(key)) {
|
|
1368
|
+
index = endIdx + 1;
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
const val = dec(valueSlice(str, eqIdx + 1, endIdx));
|
|
1372
|
+
if (allowMultiple) {
|
|
1373
|
+
const existing = obj[key];
|
|
1374
|
+
if (existing === void 0) obj[key] = val;
|
|
1375
|
+
else if (Array.isArray(existing)) existing.push(val);
|
|
1376
|
+
else obj[key] = [existing, val];
|
|
1377
|
+
} else if (obj[key] === void 0) obj[key] = val;
|
|
1378
|
+
index = endIdx + 1;
|
|
1379
|
+
} while (index < len);
|
|
1380
|
+
return obj;
|
|
1381
|
+
}
|
|
1382
|
+
function decode(str) {
|
|
1383
|
+
if (!str.includes("%")) return str;
|
|
1384
|
+
try {
|
|
1385
|
+
return decodeURIComponent(str);
|
|
1386
|
+
} catch {
|
|
1387
|
+
return str;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const cookieNameRegExp = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/;
|
|
1391
|
+
const cookieValueRegExp = /^[\u0021-\u003A\u003C-\u007E]*$/;
|
|
1392
|
+
const domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
|
1393
|
+
const pathValueRegExp = /^[\u0020-\u003A\u003C-\u007E]*$/;
|
|
1394
|
+
const __toString = Object.prototype.toString;
|
|
1395
|
+
function serialize(_a0, _a1, _a2) {
|
|
1396
|
+
const isObj = typeof _a0 === "object" && _a0 !== null;
|
|
1397
|
+
const options = isObj ? _a1 : _a2;
|
|
1398
|
+
const stringify = options?.stringify || JSON.stringify;
|
|
1399
|
+
const cookie = isObj ? _a0 : {
|
|
1400
|
+
..._a2,
|
|
1401
|
+
name: _a0,
|
|
1402
|
+
value: _a1 == void 0 ? "" : typeof _a1 === "string" ? _a1 : stringify(_a1)
|
|
1403
|
+
};
|
|
1404
|
+
const enc = options?.encode || encodeURIComponent;
|
|
1405
|
+
if (!cookieNameRegExp.test(cookie.name)) throw new TypeError(`argument name is invalid: ${cookie.name}`);
|
|
1406
|
+
const value = cookie.value ? enc(cookie.value) : "";
|
|
1407
|
+
if (!cookieValueRegExp.test(value)) throw new TypeError(`argument val is invalid: ${cookie.value}`);
|
|
1408
|
+
if (!cookie.secure) {
|
|
1409
|
+
if (cookie.partitioned) throw new TypeError(`Partitioned cookies must have the Secure attribute`);
|
|
1410
|
+
if (cookie.sameSite && String(cookie.sameSite).toLowerCase() === "none") throw new TypeError(`SameSite=None cookies must have the Secure attribute`);
|
|
1411
|
+
if (cookie.name.length > 9 && cookie.name.charCodeAt(0) === 95 && cookie.name.charCodeAt(1) === 95) {
|
|
1412
|
+
const nameLower = cookie.name.toLowerCase();
|
|
1413
|
+
if (nameLower.startsWith("__secure-") || nameLower.startsWith("__host-")) throw new TypeError(`${cookie.name} cookies must have the Secure attribute`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (cookie.name.length > 7 && cookie.name.charCodeAt(0) === 95 && cookie.name.charCodeAt(1) === 95 && cookie.name.toLowerCase().startsWith("__host-")) {
|
|
1417
|
+
if (cookie.path !== "/") throw new TypeError(`__Host- cookies must have Path=/`);
|
|
1418
|
+
if (cookie.domain) throw new TypeError(`__Host- cookies must not have a Domain attribute`);
|
|
1419
|
+
}
|
|
1420
|
+
let str = cookie.name + "=" + value;
|
|
1421
|
+
if (cookie.maxAge !== void 0) {
|
|
1422
|
+
if (!Number.isInteger(cookie.maxAge)) throw new TypeError(`option maxAge is invalid: ${cookie.maxAge}`);
|
|
1423
|
+
str += "; Max-Age=" + Math.max(0, Math.min(cookie.maxAge, COOKIE_MAX_AGE_LIMIT));
|
|
1424
|
+
}
|
|
1425
|
+
if (cookie.domain) {
|
|
1426
|
+
if (!domainValueRegExp.test(cookie.domain)) throw new TypeError(`option domain is invalid: ${cookie.domain}`);
|
|
1427
|
+
str += "; Domain=" + cookie.domain;
|
|
1428
|
+
}
|
|
1429
|
+
if (cookie.path) {
|
|
1430
|
+
if (!pathValueRegExp.test(cookie.path)) throw new TypeError(`option path is invalid: ${cookie.path}`);
|
|
1431
|
+
str += "; Path=" + cookie.path;
|
|
1432
|
+
}
|
|
1433
|
+
if (cookie.expires) {
|
|
1434
|
+
if (!isDate(cookie.expires) || !Number.isFinite(cookie.expires.valueOf())) throw new TypeError(`option expires is invalid: ${cookie.expires}`);
|
|
1435
|
+
str += "; Expires=" + cookie.expires.toUTCString();
|
|
1436
|
+
}
|
|
1437
|
+
if (cookie.httpOnly) str += "; HttpOnly";
|
|
1438
|
+
if (cookie.secure) str += "; Secure";
|
|
1439
|
+
if (cookie.partitioned) str += "; Partitioned";
|
|
1440
|
+
if (cookie.priority) switch (typeof cookie.priority === "string" ? cookie.priority.toLowerCase() : void 0) {
|
|
1441
|
+
case "low":
|
|
1442
|
+
str += "; Priority=Low";
|
|
1443
|
+
break;
|
|
1444
|
+
case "medium":
|
|
1445
|
+
str += "; Priority=Medium";
|
|
1446
|
+
break;
|
|
1447
|
+
case "high":
|
|
1448
|
+
str += "; Priority=High";
|
|
1449
|
+
break;
|
|
1450
|
+
default: throw new TypeError(`option priority is invalid: ${cookie.priority}`);
|
|
1451
|
+
}
|
|
1452
|
+
if (cookie.sameSite) switch (typeof cookie.sameSite === "string" ? cookie.sameSite.toLowerCase() : cookie.sameSite) {
|
|
1453
|
+
case true:
|
|
1454
|
+
case "strict":
|
|
1455
|
+
str += "; SameSite=Strict";
|
|
1456
|
+
break;
|
|
1457
|
+
case "lax":
|
|
1458
|
+
str += "; SameSite=Lax";
|
|
1459
|
+
break;
|
|
1460
|
+
case "none":
|
|
1461
|
+
str += "; SameSite=None";
|
|
1462
|
+
break;
|
|
1463
|
+
default: throw new TypeError(`option sameSite is invalid: ${cookie.sameSite}`);
|
|
1464
|
+
}
|
|
1465
|
+
return str;
|
|
1466
|
+
}
|
|
1467
|
+
function isDate(val) {
|
|
1468
|
+
return __toString.call(val) === "[object Date]";
|
|
1469
|
+
}
|
|
1470
|
+
const maxAgeRegExp = /^-?\d+$/;
|
|
1471
|
+
const _nullProto = /* @__PURE__ */ Object.getPrototypeOf({});
|
|
1472
|
+
function parseSetCookie(str, options) {
|
|
1473
|
+
const len = str.length;
|
|
1474
|
+
let _endIdx = len;
|
|
1475
|
+
let eqIdx = -1;
|
|
1476
|
+
for (let i = 0; i < len; i++) {
|
|
1477
|
+
const c = str.charCodeAt(i);
|
|
1478
|
+
if (c === 59) {
|
|
1479
|
+
_endIdx = i;
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
if (c === 61 && eqIdx === -1) eqIdx = i;
|
|
1483
|
+
}
|
|
1484
|
+
if (eqIdx >= _endIdx) eqIdx = -1;
|
|
1485
|
+
const name = eqIdx === -1 ? "" : _trim(str, 0, eqIdx);
|
|
1486
|
+
if (name && name in _nullProto) return void 0;
|
|
1487
|
+
let value = eqIdx === -1 ? _trim(str, 0, _endIdx) : _trim(str, eqIdx + 1, _endIdx);
|
|
1488
|
+
if (!name && !value) return void 0;
|
|
1489
|
+
if (name.length + value.length > 4096) return void 0;
|
|
1490
|
+
if (options?.decode !== false) value = _decode(value, options?.decode);
|
|
1491
|
+
const setCookie = {
|
|
1492
|
+
name,
|
|
1493
|
+
value
|
|
1494
|
+
};
|
|
1495
|
+
let index = _endIdx + 1;
|
|
1496
|
+
while (index < len) {
|
|
1497
|
+
let endIdx = len;
|
|
1498
|
+
let attrEqIdx = -1;
|
|
1499
|
+
for (let i = index; i < len; i++) {
|
|
1500
|
+
const c = str.charCodeAt(i);
|
|
1501
|
+
if (c === 59) {
|
|
1502
|
+
endIdx = i;
|
|
1503
|
+
break;
|
|
1504
|
+
}
|
|
1505
|
+
if (c === 61 && attrEqIdx === -1) attrEqIdx = i;
|
|
1506
|
+
}
|
|
1507
|
+
if (attrEqIdx >= endIdx) attrEqIdx = -1;
|
|
1508
|
+
const attr = attrEqIdx === -1 ? _trim(str, index, endIdx) : _trim(str, index, attrEqIdx);
|
|
1509
|
+
const val = attrEqIdx === -1 ? void 0 : _trim(str, attrEqIdx + 1, endIdx);
|
|
1510
|
+
if (val === void 0 || val.length <= 1024) switch (attr.toLowerCase()) {
|
|
1511
|
+
case "httponly":
|
|
1512
|
+
setCookie.httpOnly = true;
|
|
1513
|
+
break;
|
|
1514
|
+
case "secure":
|
|
1515
|
+
setCookie.secure = true;
|
|
1516
|
+
break;
|
|
1517
|
+
case "partitioned":
|
|
1518
|
+
setCookie.partitioned = true;
|
|
1519
|
+
break;
|
|
1520
|
+
case "domain":
|
|
1521
|
+
if (val) setCookie.domain = (val.charCodeAt(0) === 46 ? val.slice(1) : val).toLowerCase();
|
|
1522
|
+
break;
|
|
1523
|
+
case "path":
|
|
1524
|
+
setCookie.path = val;
|
|
1525
|
+
break;
|
|
1526
|
+
case "max-age":
|
|
1527
|
+
if (val && maxAgeRegExp.test(val)) setCookie.maxAge = Math.min(Number(val), COOKIE_MAX_AGE_LIMIT);
|
|
1528
|
+
break;
|
|
1529
|
+
case "expires": {
|
|
1530
|
+
if (!val) break;
|
|
1531
|
+
const date = new Date(val);
|
|
1532
|
+
if (Number.isFinite(date.valueOf())) {
|
|
1533
|
+
const maxDate = new Date(Date.now() + COOKIE_MAX_AGE_LIMIT * 1e3);
|
|
1534
|
+
setCookie.expires = date > maxDate ? maxDate : date;
|
|
1535
|
+
}
|
|
1536
|
+
break;
|
|
1537
|
+
}
|
|
1538
|
+
case "priority": {
|
|
1539
|
+
if (!val) break;
|
|
1540
|
+
const priority = val.toLowerCase();
|
|
1541
|
+
if (priority === "low" || priority === "medium" || priority === "high") setCookie.priority = priority;
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
case "samesite": {
|
|
1545
|
+
if (!val) break;
|
|
1546
|
+
const sameSite = val.toLowerCase();
|
|
1547
|
+
if (sameSite === "lax" || sameSite === "strict" || sameSite === "none") setCookie.sameSite = sameSite;
|
|
1548
|
+
else setCookie.sameSite = "lax";
|
|
1549
|
+
break;
|
|
1550
|
+
}
|
|
1551
|
+
default: {
|
|
1552
|
+
const attrLower = attr.toLowerCase();
|
|
1553
|
+
if (attrLower && !(attrLower in _nullProto)) setCookie[attrLower] = val;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
index = endIdx + 1;
|
|
1557
|
+
}
|
|
1558
|
+
return setCookie;
|
|
1637
1559
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
*
|
|
1649
|
-
* @param event H3 event passed by h3 handler
|
|
1650
|
-
* @param encoding The character encoding to use, defaults to 'utf-8'.
|
|
1651
|
-
*
|
|
1652
|
-
* @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body
|
|
1653
|
-
*/
|
|
1654
|
-
async function readBody(event) {
|
|
1655
|
-
const text = await event.req.text();
|
|
1656
|
-
if (!text) return void 0;
|
|
1657
|
-
const contentType = event.req.headers.get("content-type") || "";
|
|
1658
|
-
if (contentType.startsWith("application/x-www-form-urlencoded")) return parseURLEncodedBody(text);
|
|
1560
|
+
function _trim(str, start, end) {
|
|
1561
|
+
if (start === end) return "";
|
|
1562
|
+
let s = start;
|
|
1563
|
+
let e = end;
|
|
1564
|
+
while (s < e && (str.charCodeAt(s) === 32 || str.charCodeAt(s) === 9)) s++;
|
|
1565
|
+
while (e > s && (str.charCodeAt(e - 1) === 32 || str.charCodeAt(e - 1) === 9)) e--;
|
|
1566
|
+
return str.slice(s, e);
|
|
1567
|
+
}
|
|
1568
|
+
function _decode(value, decode) {
|
|
1569
|
+
if (!decode && !value.includes("%")) return value;
|
|
1659
1570
|
try {
|
|
1660
|
-
return
|
|
1571
|
+
return (decode || decodeURIComponent)(value);
|
|
1661
1572
|
} catch {
|
|
1662
|
-
|
|
1663
|
-
status: 400,
|
|
1664
|
-
statusText: "Bad Request",
|
|
1665
|
-
message: "Invalid JSON body"
|
|
1666
|
-
});
|
|
1573
|
+
return value;
|
|
1667
1574
|
}
|
|
1668
1575
|
}
|
|
1669
|
-
/**
|
|
1670
|
-
* Tries to read the request body via `readBody`, then uses the provided validation schema or function and either throws a validation error or returns the result.
|
|
1671
|
-
*
|
|
1672
|
-
* You can use a simple function to validate the body or use a Standard-Schema compatible library like `zod` to define a schema.
|
|
1673
|
-
*
|
|
1674
|
-
* @example
|
|
1675
|
-
* app.get("/", async (event) => {
|
|
1676
|
-
* const body = await readValidatedBody(event, (body) => {
|
|
1677
|
-
* return typeof body === "object" && body !== null;
|
|
1678
|
-
* });
|
|
1679
|
-
* });
|
|
1680
|
-
* @example
|
|
1681
|
-
* import { z } from "zod";
|
|
1682
|
-
*
|
|
1683
|
-
* app.get("/", async (event) => {
|
|
1684
|
-
* const objectSchema = z.object({
|
|
1685
|
-
* name: z.string().min(3).max(20),
|
|
1686
|
-
* age: z.number({ coerce: true }).positive().int(),
|
|
1687
|
-
* });
|
|
1688
|
-
* const body = await readValidatedBody(event, objectSchema);
|
|
1689
|
-
* });
|
|
1690
|
-
*
|
|
1691
|
-
* @param event The HTTPEvent passed by the handler.
|
|
1692
|
-
* @param validate The function to use for body validation. It will be called passing the read request body. If the result is not false, the parsed body will be returned.
|
|
1693
|
-
* @throws If the validation function returns `false` or throws, a validation error will be thrown.
|
|
1694
|
-
* @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body.
|
|
1695
|
-
* @see {readBody}
|
|
1696
|
-
*/
|
|
1697
|
-
async function readValidatedBody(event, validate) {
|
|
1698
|
-
const _body = await readBody(event);
|
|
1699
|
-
return validateData(_body, validate);
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
//#endregion
|
|
1703
|
-
//#region src/utils/cookie.ts
|
|
1704
1576
|
const CHUNKED_COOKIE = "__chunked__";
|
|
1705
1577
|
const CHUNKS_MAX_LENGTH = 4e3;
|
|
1706
|
-
/**
|
|
1707
|
-
* Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs.
|
|
1708
|
-
* @param event {HTTPEvent} H3 event or req passed by h3 handler
|
|
1709
|
-
* @returns Object of cookie name-value pairs
|
|
1710
|
-
* ```ts
|
|
1711
|
-
* const cookies = parseCookies(event)
|
|
1712
|
-
* ```
|
|
1713
|
-
*/
|
|
1714
1578
|
function parseCookies(event) {
|
|
1715
1579
|
return parse(event.req.headers.get("cookie") || "");
|
|
1716
1580
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
* @param name Name of the cookie to get
|
|
1721
|
-
* @returns {*} Value of the cookie (String or undefined)
|
|
1722
|
-
* ```ts
|
|
1723
|
-
* const authorization = getCookie(request, 'Authorization')
|
|
1724
|
-
* ```
|
|
1725
|
-
*/
|
|
1581
|
+
function getValidatedCookies(event, validate, options) {
|
|
1582
|
+
return validateData(parseCookies(event), validate, options);
|
|
1583
|
+
}
|
|
1726
1584
|
function getCookie(event, name) {
|
|
1727
1585
|
return parseCookies(event)[name];
|
|
1728
1586
|
}
|
|
1729
|
-
/**
|
|
1730
|
-
* Set a cookie value by name.
|
|
1731
|
-
* @param event {H3Event} H3 event or res passed by h3 handler
|
|
1732
|
-
* @param name Name of the cookie to set
|
|
1733
|
-
* @param value Value of the cookie to set
|
|
1734
|
-
* @param options {CookieSerializeOptions} Options for serializing the cookie
|
|
1735
|
-
* ```ts
|
|
1736
|
-
* setCookie(res, 'Authorization', '1234567')
|
|
1737
|
-
* ```
|
|
1738
|
-
*/
|
|
1739
1587
|
function setCookie(event, name, value, options) {
|
|
1740
|
-
const
|
|
1588
|
+
const { encode, stringify, ...attrs } = options ?? {};
|
|
1589
|
+
const newCookie = serialize({
|
|
1590
|
+
name,
|
|
1591
|
+
value,
|
|
1741
1592
|
path: "/",
|
|
1742
|
-
...
|
|
1593
|
+
...attrs
|
|
1594
|
+
}, {
|
|
1595
|
+
encode,
|
|
1596
|
+
stringify
|
|
1743
1597
|
});
|
|
1744
1598
|
const currentCookies = event.res.headers.getSetCookie();
|
|
1745
1599
|
if (currentCookies.length === 0) {
|
|
@@ -1749,59 +1603,32 @@ function setCookie(event, name, value, options) {
|
|
|
1749
1603
|
const newCookieKey = _getDistinctCookieKey(name, options || {});
|
|
1750
1604
|
event.res.headers.delete("set-cookie");
|
|
1751
1605
|
for (const cookie of currentCookies) {
|
|
1752
|
-
const
|
|
1753
|
-
if (
|
|
1606
|
+
const parsed = parseSetCookie(cookie);
|
|
1607
|
+
if (!parsed) continue;
|
|
1608
|
+
if (_getDistinctCookieKey(cookie.split("=")?.[0], parsed) === newCookieKey) continue;
|
|
1754
1609
|
event.res.headers.append("set-cookie", cookie);
|
|
1755
1610
|
}
|
|
1756
1611
|
event.res.headers.append("set-cookie", newCookie);
|
|
1757
1612
|
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Remove a cookie by name.
|
|
1760
|
-
* @param event {H3Event} H3 event or res passed by h3 handler
|
|
1761
|
-
* @param name Name of the cookie to delete
|
|
1762
|
-
* @param serializeOptions {CookieSerializeOptions} Cookie options
|
|
1763
|
-
* ```ts
|
|
1764
|
-
* deleteCookie(res, 'SessionId')
|
|
1765
|
-
* ```
|
|
1766
|
-
*/
|
|
1767
1613
|
function deleteCookie(event, name, serializeOptions) {
|
|
1768
1614
|
setCookie(event, name, "", {
|
|
1769
1615
|
...serializeOptions,
|
|
1770
1616
|
maxAge: 0
|
|
1771
1617
|
});
|
|
1772
1618
|
}
|
|
1773
|
-
/**
|
|
1774
|
-
* Get a chunked cookie value by name. Will join chunks together.
|
|
1775
|
-
* @param event {HTTPEvent} { req: Request }
|
|
1776
|
-
* @param name Name of the cookie to get
|
|
1777
|
-
* @returns {*} Value of the cookie (String or undefined)
|
|
1778
|
-
* ```ts
|
|
1779
|
-
* const authorization = getCookie(request, 'Session')
|
|
1780
|
-
* ```
|
|
1781
|
-
*/
|
|
1782
1619
|
function getChunkedCookie(event, name) {
|
|
1783
1620
|
const mainCookie = getCookie(event, name);
|
|
1784
1621
|
if (!mainCookie || !mainCookie.startsWith(CHUNKED_COOKIE)) return mainCookie;
|
|
1785
1622
|
const chunksCount = getChunkedCookieCount(mainCookie);
|
|
1786
|
-
if (chunksCount === 0) return
|
|
1623
|
+
if (chunksCount === 0) return;
|
|
1787
1624
|
const chunks = [];
|
|
1788
1625
|
for (let i = 1; i <= chunksCount; i++) {
|
|
1789
1626
|
const chunk = getCookie(event, chunkCookieName(name, i));
|
|
1790
|
-
if (!chunk) return
|
|
1627
|
+
if (!chunk) return;
|
|
1791
1628
|
chunks.push(chunk);
|
|
1792
1629
|
}
|
|
1793
1630
|
return chunks.join("");
|
|
1794
1631
|
}
|
|
1795
|
-
/**
|
|
1796
|
-
* Set a cookie value by name. Chunked cookies will be created as needed.
|
|
1797
|
-
* @param event {H3Event} H3 event or res passed by h3 handler
|
|
1798
|
-
* @param name Name of the cookie to set
|
|
1799
|
-
* @param value Value of the cookie to set
|
|
1800
|
-
* @param options {CookieSerializeOptions} Options for serializing the cookie
|
|
1801
|
-
* ```ts
|
|
1802
|
-
* setCookie(res, 'Session', '<session data>')
|
|
1803
|
-
* ```
|
|
1804
|
-
*/
|
|
1805
1632
|
function setChunkedCookie(event, name, value, options) {
|
|
1806
1633
|
const chunkMaxLength = options?.chunkMaxLength || CHUNKS_MAX_LENGTH;
|
|
1807
1634
|
const chunkCount = Math.ceil(value.length / chunkMaxLength);
|
|
@@ -1814,8 +1641,7 @@ function setChunkedCookie(event, name, value, options) {
|
|
|
1814
1641
|
setCookie(event, name, value, options);
|
|
1815
1642
|
return;
|
|
1816
1643
|
}
|
|
1817
|
-
|
|
1818
|
-
setCookie(event, name, mainCookieValue, options);
|
|
1644
|
+
setCookie(event, name, `${CHUNKED_COOKIE}${chunkCount}`, options);
|
|
1819
1645
|
for (let i = 1; i <= chunkCount; i++) {
|
|
1820
1646
|
const start = (i - 1) * chunkMaxLength;
|
|
1821
1647
|
const end = start + chunkMaxLength;
|
|
@@ -1823,26 +1649,12 @@ function setChunkedCookie(event, name, value, options) {
|
|
|
1823
1649
|
setCookie(event, chunkCookieName(name, i), chunkValue, options);
|
|
1824
1650
|
}
|
|
1825
1651
|
}
|
|
1826
|
-
/**
|
|
1827
|
-
* Remove a set of chunked cookies by name.
|
|
1828
|
-
* @param event {H3Event} H3 event or res passed by h3 handler
|
|
1829
|
-
* @param name Name of the cookie to delete
|
|
1830
|
-
* @param serializeOptions {CookieSerializeOptions} Cookie options
|
|
1831
|
-
* ```ts
|
|
1832
|
-
* deleteCookie(res, 'Session')
|
|
1833
|
-
* ```
|
|
1834
|
-
*/
|
|
1835
1652
|
function deleteChunkedCookie(event, name, serializeOptions) {
|
|
1836
1653
|
const mainCookie = getCookie(event, name);
|
|
1837
1654
|
deleteCookie(event, name, serializeOptions);
|
|
1838
1655
|
const chunksCount = getChunkedCookieCount(mainCookie);
|
|
1839
1656
|
if (chunksCount >= 0) for (let i = 0; i < chunksCount; i++) deleteCookie(event, chunkCookieName(name, i + 1), serializeOptions);
|
|
1840
1657
|
}
|
|
1841
|
-
/**
|
|
1842
|
-
* Cookies are unique by "cookie-name, domain-value, and path-value".
|
|
1843
|
-
*
|
|
1844
|
-
* @see https://httpwg.org/specs/rfc6265.html#rfc.section.4.1.2
|
|
1845
|
-
*/
|
|
1846
1658
|
function _getDistinctCookieKey(name, options) {
|
|
1847
1659
|
return [
|
|
1848
1660
|
name,
|
|
@@ -1850,19 +1662,17 @@ function _getDistinctCookieKey(name, options) {
|
|
|
1850
1662
|
options.path || "/"
|
|
1851
1663
|
].join(";");
|
|
1852
1664
|
}
|
|
1665
|
+
const MAX_CHUNKED_COOKIE_COUNT = 100;
|
|
1853
1666
|
function getChunkedCookieCount(cookie) {
|
|
1854
|
-
if (!cookie?.startsWith(CHUNKED_COOKIE)) return
|
|
1855
|
-
|
|
1667
|
+
if (!cookie?.startsWith(CHUNKED_COOKIE)) return NaN;
|
|
1668
|
+
const count = Number.parseInt(cookie.slice(11));
|
|
1669
|
+
if (Number.isNaN(count) || count < 0 || count > MAX_CHUNKED_COOKIE_COUNT) return NaN;
|
|
1670
|
+
return count;
|
|
1856
1671
|
}
|
|
1857
1672
|
function chunkCookieName(name, chunkNumber) {
|
|
1858
1673
|
return `${name}.${chunkNumber}`;
|
|
1859
1674
|
}
|
|
1860
|
-
|
|
1861
|
-
//#endregion
|
|
1862
|
-
//#region src/utils/internal/event-stream.ts
|
|
1863
|
-
/**
|
|
1864
|
-
* A helper class for [server sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format)
|
|
1865
|
-
*/
|
|
1675
|
+
const _noop = () => {};
|
|
1866
1676
|
var EventStream = class {
|
|
1867
1677
|
_event;
|
|
1868
1678
|
_transformStream = new TransformStream();
|
|
@@ -1876,7 +1686,7 @@ var EventStream = class {
|
|
|
1876
1686
|
constructor(event, opts = {}) {
|
|
1877
1687
|
this._event = event;
|
|
1878
1688
|
this._writer = this._transformStream.writable.getWriter();
|
|
1879
|
-
this._writer.closed.
|
|
1689
|
+
this._writer.closed.catch(_noop).finally(() => {
|
|
1880
1690
|
this._writerIsClosed = true;
|
|
1881
1691
|
});
|
|
1882
1692
|
if (opts.autoclose !== false) this._event.runtime?.node?.res?.once("close", () => this.close());
|
|
@@ -1909,7 +1719,9 @@ var EventStream = class {
|
|
|
1909
1719
|
this._unsentData += formatEventStreamComment(comment);
|
|
1910
1720
|
return;
|
|
1911
1721
|
}
|
|
1912
|
-
await this._writer.write(this._encoder.encode(formatEventStreamComment(comment))).catch()
|
|
1722
|
+
await this._writer.write(this._encoder.encode(formatEventStreamComment(comment))).catch(() => {
|
|
1723
|
+
this._writerIsClosed = true;
|
|
1724
|
+
});
|
|
1913
1725
|
}
|
|
1914
1726
|
async _sendEvent(message) {
|
|
1915
1727
|
if (this._writerIsClosed) return;
|
|
@@ -1921,7 +1733,9 @@ var EventStream = class {
|
|
|
1921
1733
|
this._unsentData += formatEventStreamMessage(message);
|
|
1922
1734
|
return;
|
|
1923
1735
|
}
|
|
1924
|
-
await this._writer.write(this._encoder.encode(formatEventStreamMessage(message))).catch()
|
|
1736
|
+
await this._writer.write(this._encoder.encode(formatEventStreamMessage(message))).catch(() => {
|
|
1737
|
+
this._writerIsClosed = true;
|
|
1738
|
+
});
|
|
1925
1739
|
}
|
|
1926
1740
|
async _sendEvents(messages) {
|
|
1927
1741
|
if (this._writerIsClosed) return;
|
|
@@ -1934,7 +1748,9 @@ var EventStream = class {
|
|
|
1934
1748
|
this._unsentData += payload;
|
|
1935
1749
|
return;
|
|
1936
1750
|
}
|
|
1937
|
-
await this._writer.write(this._encoder.encode(payload)).catch()
|
|
1751
|
+
await this._writer.write(this._encoder.encode(payload)).catch(() => {
|
|
1752
|
+
this._writerIsClosed = true;
|
|
1753
|
+
});
|
|
1938
1754
|
}
|
|
1939
1755
|
pause() {
|
|
1940
1756
|
this._paused = true;
|
|
@@ -1949,13 +1765,12 @@ var EventStream = class {
|
|
|
1949
1765
|
async flush() {
|
|
1950
1766
|
if (this._writerIsClosed) return;
|
|
1951
1767
|
if (this._unsentData?.length) {
|
|
1952
|
-
await this._writer.write(this._encoder.encode(this._unsentData))
|
|
1768
|
+
await this._writer.write(this._encoder.encode(this._unsentData)).catch(() => {
|
|
1769
|
+
this._writerIsClosed = true;
|
|
1770
|
+
});
|
|
1953
1771
|
this._unsentData = void 0;
|
|
1954
1772
|
}
|
|
1955
1773
|
}
|
|
1956
|
-
/**
|
|
1957
|
-
* Close the stream and the connection if the stream is being sent to the client
|
|
1958
|
-
*/
|
|
1959
1774
|
async close() {
|
|
1960
1775
|
if (this._disposed) return;
|
|
1961
1776
|
if (!this._writerIsClosed) try {
|
|
@@ -1963,12 +1778,8 @@ var EventStream = class {
|
|
|
1963
1778
|
} catch {}
|
|
1964
1779
|
this._disposed = true;
|
|
1965
1780
|
}
|
|
1966
|
-
/**
|
|
1967
|
-
* Triggers callback when the writable stream is closed.
|
|
1968
|
-
* It is also triggered after calling the `close()` method.
|
|
1969
|
-
*/
|
|
1970
1781
|
onClosed(cb) {
|
|
1971
|
-
this._writer.closed.then(cb);
|
|
1782
|
+
this._writer.closed.then(cb).catch(_noop);
|
|
1972
1783
|
}
|
|
1973
1784
|
async send() {
|
|
1974
1785
|
setEventStreamHeaders(this._event);
|
|
@@ -1978,16 +1789,21 @@ var EventStream = class {
|
|
|
1978
1789
|
}
|
|
1979
1790
|
};
|
|
1980
1791
|
function formatEventStreamComment(comment) {
|
|
1981
|
-
return `: ${
|
|
1792
|
+
return comment.split(/\r\n|\r|\n/).map((l) => `: ${l}\n`).join("") + "\n";
|
|
1982
1793
|
}
|
|
1983
1794
|
function formatEventStreamMessage(message) {
|
|
1984
1795
|
let result = "";
|
|
1985
|
-
if (message.id) result += `id: ${message.id}\n`;
|
|
1986
|
-
if (message.event) result += `event: ${message.event}\n`;
|
|
1796
|
+
if (message.id) result += `id: ${_sanitizeSingleLine(message.id)}\n`;
|
|
1797
|
+
if (message.event) result += `event: ${_sanitizeSingleLine(message.event)}\n`;
|
|
1987
1798
|
if (typeof message.retry === "number" && Number.isInteger(message.retry)) result += `retry: ${message.retry}\n`;
|
|
1988
|
-
|
|
1799
|
+
const data = typeof message.data === "string" ? message.data : "";
|
|
1800
|
+
for (const line of data.split(/\r\n|\r|\n/)) result += `data: ${line}\n`;
|
|
1801
|
+
result += "\n";
|
|
1989
1802
|
return result;
|
|
1990
1803
|
}
|
|
1804
|
+
function _sanitizeSingleLine(value) {
|
|
1805
|
+
return value.replace(/[\n\r]/g, "");
|
|
1806
|
+
}
|
|
1991
1807
|
function formatEventStreamMessages(messages) {
|
|
1992
1808
|
let result = "";
|
|
1993
1809
|
for (const msg of messages) result += formatEventStreamMessage(msg);
|
|
@@ -1999,63 +1815,50 @@ function setEventStreamHeaders(event) {
|
|
|
1999
1815
|
event.res.headers.set("x-accel-buffering", "no");
|
|
2000
1816
|
if (event.req.headers.get("connection") === "keep-alive") event.res.headers.set("connection", "keep-alive");
|
|
2001
1817
|
}
|
|
2002
|
-
|
|
2003
|
-
//#endregion
|
|
2004
|
-
//#region src/utils/event-stream.ts
|
|
2005
|
-
/**
|
|
2006
|
-
* Initialize an EventStream instance for creating [server sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
|
|
2007
|
-
*
|
|
2008
|
-
* @experimental This function is experimental and might be unstable in some environments.
|
|
2009
|
-
*
|
|
2010
|
-
* @example
|
|
2011
|
-
*
|
|
2012
|
-
* ```ts
|
|
2013
|
-
* import { createEventStream, sendEventStream } from "h3";
|
|
2014
|
-
*
|
|
2015
|
-
* app.get("/sse", (event) => {
|
|
2016
|
-
* const eventStream = createEventStream(event);
|
|
2017
|
-
*
|
|
2018
|
-
* // Send a message every second
|
|
2019
|
-
* const interval = setInterval(async () => {
|
|
2020
|
-
* await eventStream.push("Hello world");
|
|
2021
|
-
* }, 1000);
|
|
2022
|
-
*
|
|
2023
|
-
* // cleanup the interval and close the stream when the connection is terminated
|
|
2024
|
-
* eventStream.onClosed(async () => {
|
|
2025
|
-
* console.log("closing SSE...");
|
|
2026
|
-
* clearInterval(interval);
|
|
2027
|
-
* await eventStream.close();
|
|
2028
|
-
* });
|
|
2029
|
-
*
|
|
2030
|
-
* return eventStream.send();
|
|
2031
|
-
* });
|
|
2032
|
-
* ```
|
|
2033
|
-
*/
|
|
2034
1818
|
function createEventStream(event, opts) {
|
|
2035
1819
|
return new EventStream(event, opts);
|
|
2036
1820
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
1821
|
+
function setServerTiming(event, name, opts) {
|
|
1822
|
+
if (!_isValidToken(name)) throw new TypeError(`Invalid Server-Timing metric name: ${name}`);
|
|
1823
|
+
if (opts?.dur !== void 0 && (!Number.isFinite(opts.dur) || opts.dur < 0)) throw new TypeError(`Invalid Server-Timing duration: ${opts.dur}`);
|
|
1824
|
+
const value = name + (opts?.desc ? `;desc="${_escapeDesc(opts.desc)}"` : "") + (opts?.dur !== void 0 ? `;dur=${opts.dur}` : "");
|
|
1825
|
+
event.res.headers.append("server-timing", value);
|
|
1826
|
+
const ctx = event.context;
|
|
1827
|
+
if (!Array.isArray(ctx.timing)) ctx.timing = [];
|
|
1828
|
+
ctx.timing.push({
|
|
1829
|
+
name,
|
|
1830
|
+
...opts
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
async function withServerTiming(event, name, fn) {
|
|
1834
|
+
const start = performance.now();
|
|
1835
|
+
try {
|
|
1836
|
+
return await fn();
|
|
1837
|
+
} finally {
|
|
1838
|
+
setServerTiming(event, name, { dur: performance.now() - start });
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
const _tokenRE = /^[\w!#$%&'*+.^`|~-]+$/;
|
|
1842
|
+
function _isValidToken(value) {
|
|
1843
|
+
return _tokenRE.test(value);
|
|
1844
|
+
}
|
|
1845
|
+
function _escapeDesc(value) {
|
|
1846
|
+
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
1847
|
+
}
|
|
2045
1848
|
function handleCacheHeaders(event, opts) {
|
|
2046
1849
|
const cacheControls = ["public", ...opts.cacheControls || []];
|
|
2047
1850
|
let cacheMatched = false;
|
|
2048
1851
|
if (opts.maxAge !== void 0) cacheControls.push(`max-age=${+opts.maxAge}`, `s-maxage=${+opts.maxAge}`);
|
|
2049
1852
|
if (opts.modifiedTime) {
|
|
2050
1853
|
const modifiedTime = new Date(opts.modifiedTime);
|
|
1854
|
+
modifiedTime.setMilliseconds(0);
|
|
2051
1855
|
const ifModifiedSince = event.req.headers.get("if-modified-since");
|
|
2052
1856
|
event.res.headers.set("last-modified", modifiedTime.toUTCString());
|
|
2053
1857
|
if (ifModifiedSince && new Date(ifModifiedSince) >= modifiedTime) cacheMatched = true;
|
|
2054
1858
|
}
|
|
2055
1859
|
if (opts.etag) {
|
|
2056
1860
|
event.res.headers.set("etag", opts.etag);
|
|
2057
|
-
|
|
2058
|
-
if (ifNonMatch === opts.etag) cacheMatched = true;
|
|
1861
|
+
if (event.req.headers.get("if-none-match") === opts.etag) cacheMatched = true;
|
|
2059
1862
|
}
|
|
2060
1863
|
event.res.headers.set("cache-control", cacheControls.join(", "));
|
|
2061
1864
|
if (cacheMatched) {
|
|
@@ -2064,27 +1867,6 @@ function handleCacheHeaders(event, opts) {
|
|
|
2064
1867
|
}
|
|
2065
1868
|
return false;
|
|
2066
1869
|
}
|
|
2067
|
-
|
|
2068
|
-
//#endregion
|
|
2069
|
-
//#region src/utils/internal/path.ts
|
|
2070
|
-
function withLeadingSlash(path) {
|
|
2071
|
-
if (!path || path === "/") return "/";
|
|
2072
|
-
return path[0] === "/" ? path : `/${path}`;
|
|
2073
|
-
}
|
|
2074
|
-
function withoutTrailingSlash(path) {
|
|
2075
|
-
if (!path || path === "/") return "/";
|
|
2076
|
-
return path[path.length - 1] === "/" ? path.slice(0, -1) : path;
|
|
2077
|
-
}
|
|
2078
|
-
function withoutBase(input = "", base = "") {
|
|
2079
|
-
if (!base || base === "/") return input;
|
|
2080
|
-
const _base = withoutTrailingSlash(base);
|
|
2081
|
-
if (!input.startsWith(_base)) return input;
|
|
2082
|
-
const trimmed = input.slice(_base.length);
|
|
2083
|
-
return trimmed[0] === "/" ? trimmed : "/" + trimmed;
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
//#endregion
|
|
2087
|
-
//#region src/utils/internal/mime.ts
|
|
2088
1870
|
const COMMON_MIME_TYPES = {
|
|
2089
1871
|
".html": "text/html",
|
|
2090
1872
|
".htm": "text/html",
|
|
@@ -2116,12 +1898,6 @@ function getExtension(path) {
|
|
|
2116
1898
|
function getType(ext) {
|
|
2117
1899
|
return ext ? COMMON_MIME_TYPES[ext] : void 0;
|
|
2118
1900
|
}
|
|
2119
|
-
|
|
2120
|
-
//#endregion
|
|
2121
|
-
//#region src/utils/static.ts
|
|
2122
|
-
/**
|
|
2123
|
-
* Dynamically serve static assets based on the request path.
|
|
2124
|
-
*/
|
|
2125
1901
|
async function serveStatic(event, options) {
|
|
2126
1902
|
if (options.headers) {
|
|
2127
1903
|
const entries = Array.isArray(options.headers) ? options.headers : typeof options.headers.entries === "function" ? options.headers.entries() : Object.entries(options.headers);
|
|
@@ -2132,7 +1908,7 @@ async function serveStatic(event, options) {
|
|
|
2132
1908
|
event.res.headers.set("allow", "GET, HEAD");
|
|
2133
1909
|
throw new HTTPError({ status: 405 });
|
|
2134
1910
|
}
|
|
2135
|
-
const originalId = decodeURI(withLeadingSlash(withoutTrailingSlash(event.url.pathname)));
|
|
1911
|
+
const originalId = resolveDotSegments(decodeURI(withLeadingSlash(withoutTrailingSlash(event.url.pathname))));
|
|
2136
1912
|
const acceptEncodings = parseAcceptEncoding(event.req.headers.get("accept-encoding") || "", options.encodings);
|
|
2137
1913
|
if (acceptEncodings.length > 1) event.res.headers.set("vary", "accept-encoding");
|
|
2138
1914
|
let id = originalId;
|
|
@@ -2160,8 +1936,7 @@ async function serveStatic(event, options) {
|
|
|
2160
1936
|
if (!event.res.headers.get("last-modified")) event.res.headers.set("last-modified", mtimeDate.toUTCString());
|
|
2161
1937
|
}
|
|
2162
1938
|
if (meta.etag && !event.res.headers.has("etag")) event.res.headers.set("etag", meta.etag);
|
|
2163
|
-
|
|
2164
|
-
if (ifNotMatch) return new HTTPResponse(null, {
|
|
1939
|
+
if (meta.etag && event.req.headers.get("if-none-match") === meta.etag) return new HTTPResponse(null, {
|
|
2165
1940
|
status: 304,
|
|
2166
1941
|
statusText: "Not Modified"
|
|
2167
1942
|
});
|
|
@@ -2174,8 +1949,7 @@ async function serveStatic(event, options) {
|
|
|
2174
1949
|
if (meta.encoding && !event.res.headers.get("content-encoding")) event.res.headers.set("content-encoding", meta.encoding);
|
|
2175
1950
|
if (meta.size !== void 0 && meta.size > 0 && !event.req.headers.get("content-length")) event.res.headers.set("content-length", meta.size + "");
|
|
2176
1951
|
if (event.req.method === "HEAD") return new HTTPResponse(null, { status: 200 });
|
|
2177
|
-
|
|
2178
|
-
return new HTTPResponse(contents || null, { status: 200 });
|
|
1952
|
+
return new HTTPResponse(await options.getContents(id) || null, { status: 200 });
|
|
2179
1953
|
}
|
|
2180
1954
|
function parseAcceptEncoding(header, encodingMap) {
|
|
2181
1955
|
if (!encodingMap || !header) return [];
|
|
@@ -2186,21 +1960,6 @@ function idSearchPaths(id, encodings, indexNames) {
|
|
|
2186
1960
|
for (const suffix of ["", ...indexNames]) for (const encoding of [...encodings, ""]) ids.push(`${id}${suffix}${encoding}`);
|
|
2187
1961
|
return ids;
|
|
2188
1962
|
}
|
|
2189
|
-
|
|
2190
|
-
//#endregion
|
|
2191
|
-
//#region src/utils/base.ts
|
|
2192
|
-
/**
|
|
2193
|
-
* Returns a new event handler that removes the base url of the event before calling the original handler.
|
|
2194
|
-
*
|
|
2195
|
-
* @example
|
|
2196
|
-
* const api = new H3()
|
|
2197
|
-
* .get("/", () => "Hello API!");
|
|
2198
|
-
* const app = new H3();
|
|
2199
|
-
* .use("/api/**", withBase("/api", api.handler));
|
|
2200
|
-
*
|
|
2201
|
-
* @param base The base path to prefix.
|
|
2202
|
-
* @param handler The event handler to use with the adapted path.
|
|
2203
|
-
*/
|
|
2204
1963
|
function withBase(base, input) {
|
|
2205
1964
|
base = withoutTrailingSlash(base);
|
|
2206
1965
|
const handler = toEventHandler(input);
|
|
@@ -2215,10 +1974,6 @@ function withBase(base, input) {
|
|
|
2215
1974
|
}
|
|
2216
1975
|
};
|
|
2217
1976
|
}
|
|
2218
|
-
|
|
2219
|
-
//#endregion
|
|
2220
|
-
//#region src/utils/internal/iron-crypto.ts
|
|
2221
|
-
/** The default encryption and integrity settings. */
|
|
2222
1977
|
const defaults = /* @__PURE__ */ Object.freeze({
|
|
2223
1978
|
ttl: 0,
|
|
2224
1979
|
timestampSkewSec: 60,
|
|
@@ -2236,7 +1991,6 @@ const defaults = /* @__PURE__ */ Object.freeze({
|
|
|
2236
1991
|
minPasswordlength: 32
|
|
2237
1992
|
})
|
|
2238
1993
|
});
|
|
2239
|
-
/** Configuration of each supported algorithm. */
|
|
2240
1994
|
const algorithms = /* @__PURE__ */ Object.freeze({
|
|
2241
1995
|
"aes-128-ctr": /* @__PURE__ */ Object.freeze({
|
|
2242
1996
|
keyBits: 128,
|
|
@@ -2254,9 +2008,7 @@ const algorithms = /* @__PURE__ */ Object.freeze({
|
|
|
2254
2008
|
name: "SHA-256"
|
|
2255
2009
|
})
|
|
2256
2010
|
});
|
|
2257
|
-
/** MAC normalization prefix. */
|
|
2258
2011
|
const macPrefix = "Fe26.2";
|
|
2259
|
-
/** Serializes, encrypts, and signs objects into an iron protocol string. */
|
|
2260
2012
|
async function seal(object, password, opts) {
|
|
2261
2013
|
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
|
|
2262
2014
|
if (!password) throw new Error("Empty password");
|
|
@@ -2268,10 +2020,8 @@ async function seal(object, password, opts) {
|
|
|
2268
2020
|
const expiration = opts.ttl ? now + opts.ttl : "";
|
|
2269
2021
|
const macBaseString = `${macPrefix}*${id}*${key.salt}*${iv}*${encryptedB64}*${expiration}`;
|
|
2270
2022
|
const mac = await hmacWithPassword(integrity, opts.integrity, macBaseString);
|
|
2271
|
-
|
|
2272
|
-
return sealed;
|
|
2023
|
+
return `${macBaseString}*${mac.salt}*${mac.digest}`;
|
|
2273
2024
|
}
|
|
2274
|
-
/** Verifies, decrypts, and reconstruct an iron protocol string into an object. */
|
|
2275
2025
|
async function unseal(sealed, password, opts) {
|
|
2276
2026
|
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
|
|
2277
2027
|
if (!password) throw new Error("Empty password");
|
|
@@ -2279,11 +2029,10 @@ async function unseal(sealed, password, opts) {
|
|
|
2279
2029
|
if (parts.length !== 8) throw new Error("Incorrect number of sealed components");
|
|
2280
2030
|
const [prefix, passwordId, encryptionSalt, encryptionIv, encryptedB64, expiration, hmacSalt, hmac] = parts;
|
|
2281
2031
|
const macBaseString = `${prefix}*${passwordId}*${encryptionSalt}*${encryptionIv}*${encryptedB64}*${expiration}`;
|
|
2282
|
-
if (
|
|
2032
|
+
if ("Fe26.2" !== prefix) throw new Error("Wrong mac prefix");
|
|
2283
2033
|
if (expiration) {
|
|
2284
2034
|
if (!/^\d+$/.test(expiration)) throw new Error("Invalid expiration");
|
|
2285
|
-
|
|
2286
|
-
if (exp <= now - opts.timestampSkewSec * 1e3) throw new Error("Expired seal");
|
|
2035
|
+
if (Number.parseInt(expiration, 10) <= now - opts.timestampSkewSec * 1e3) throw new Error("Expired seal");
|
|
2287
2036
|
}
|
|
2288
2037
|
let pass = "";
|
|
2289
2038
|
const _passwordId = passwordId || "default";
|
|
@@ -2291,11 +2040,10 @@ async function unseal(sealed, password, opts) {
|
|
|
2291
2040
|
else if (_passwordId in password) pass = password[_passwordId];
|
|
2292
2041
|
else throw new Error(`Cannot find password: ${_passwordId}`);
|
|
2293
2042
|
pass = normalizePassword(pass);
|
|
2294
|
-
|
|
2043
|
+
if (!fixedTimeComparison((await hmacWithPassword(pass.integrity, {
|
|
2295
2044
|
...opts.integrity,
|
|
2296
2045
|
salt: hmacSalt
|
|
2297
|
-
}, macBaseString);
|
|
2298
|
-
if (!fixedTimeComparison(mac.digest, hmac)) throw new Error("Bad hmac value");
|
|
2046
|
+
}, macBaseString)).digest, hmac)) throw new Error("Bad hmac value");
|
|
2299
2047
|
const encrypted = base64Decode(encryptedB64);
|
|
2300
2048
|
const decryptOptions = {
|
|
2301
2049
|
...opts.encryption,
|
|
@@ -2305,7 +2053,6 @@ async function unseal(sealed, password, opts) {
|
|
|
2305
2053
|
const decrypted = await decrypt(pass.encryption, decryptOptions, encrypted);
|
|
2306
2054
|
return decrypted ? JSON.parse(decrypted) : null;
|
|
2307
2055
|
}
|
|
2308
|
-
/** Calculates a HMAC digest. */
|
|
2309
2056
|
async function hmacWithPassword(password, options, data) {
|
|
2310
2057
|
const key = await generateKey(password, {
|
|
2311
2058
|
...options,
|
|
@@ -2313,13 +2060,11 @@ async function hmacWithPassword(password, options, data) {
|
|
|
2313
2060
|
});
|
|
2314
2061
|
const textBuffer = textEncoder.encode(data);
|
|
2315
2062
|
const signed = await crypto.subtle.sign({ name: "HMAC" }, key.key, textBuffer);
|
|
2316
|
-
const digest = base64Encode(new Uint8Array(signed));
|
|
2317
2063
|
return {
|
|
2318
|
-
digest,
|
|
2064
|
+
digest: base64Encode(new Uint8Array(signed)),
|
|
2319
2065
|
salt: key.salt
|
|
2320
2066
|
};
|
|
2321
2067
|
}
|
|
2322
|
-
/** Generates a key from the password. */
|
|
2323
2068
|
async function generateKey(password, options) {
|
|
2324
2069
|
if (!password?.length) throw new Error("Empty password");
|
|
2325
2070
|
if (options == null || typeof options !== "object") throw new Error("Bad options");
|
|
@@ -2344,8 +2089,7 @@ async function generateKey(password, options) {
|
|
|
2344
2089
|
salt = [...new Uint8Array(randomSalt)].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
2345
2090
|
}
|
|
2346
2091
|
const derivedKey = await pbkdf2(password, salt, options.iterations, algorithm.keyBits / 8, "SHA-1");
|
|
2347
|
-
|
|
2348
|
-
resultKey = importedEncryptionKey;
|
|
2092
|
+
resultKey = await crypto.subtle.importKey("raw", derivedKey, id, false, usage);
|
|
2349
2093
|
resultSalt = salt;
|
|
2350
2094
|
} else {
|
|
2351
2095
|
if (password.length < algorithm.keyBits / 8) throw new Error("Key buffer (password) too small");
|
|
@@ -2361,19 +2105,16 @@ async function generateKey(password, options) {
|
|
|
2361
2105
|
iv: resultIV
|
|
2362
2106
|
};
|
|
2363
2107
|
}
|
|
2364
|
-
/** Provides an asynchronous Password-Based Key Derivation Function 2 (PBKDF2) implementation. */
|
|
2365
2108
|
async function pbkdf2(password, salt, iterations, keyLength, hash) {
|
|
2366
2109
|
const passwordBuffer = textEncoder.encode(password);
|
|
2367
2110
|
const importedKey = await crypto.subtle.importKey("raw", passwordBuffer, { name: "PBKDF2" }, false, ["deriveBits"]);
|
|
2368
|
-
const saltBuffer = textEncoder.encode(salt);
|
|
2369
2111
|
const params = {
|
|
2370
2112
|
name: "PBKDF2",
|
|
2371
2113
|
hash,
|
|
2372
|
-
salt:
|
|
2114
|
+
salt: textEncoder.encode(salt),
|
|
2373
2115
|
iterations
|
|
2374
2116
|
};
|
|
2375
|
-
|
|
2376
|
-
return derivation;
|
|
2117
|
+
return await crypto.subtle.deriveBits(params, importedKey, keyLength * 8);
|
|
2377
2118
|
}
|
|
2378
2119
|
async function encrypt(password, options, data) {
|
|
2379
2120
|
const key = await generateKey(password, options);
|
|
@@ -2402,14 +2143,12 @@ function getEncryptParams(algorithm, key, data) {
|
|
|
2402
2143
|
typeof data === "string" ? textEncoder.encode(data) : data
|
|
2403
2144
|
];
|
|
2404
2145
|
}
|
|
2405
|
-
/** Returns true if `a` is equal to `b`, without leaking timing information that would allow an attacker to guess one of the values. */
|
|
2406
2146
|
function fixedTimeComparison(a, b) {
|
|
2407
2147
|
let mismatch = a.length === b.length ? 0 : 1;
|
|
2408
2148
|
if (mismatch) b = a;
|
|
2409
2149
|
for (let i = 0; i < a.length; i += 1) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2410
2150
|
return mismatch === 0;
|
|
2411
2151
|
}
|
|
2412
|
-
/** Normalizes a password parameter. */
|
|
2413
2152
|
function normalizePassword(password) {
|
|
2414
2153
|
if (typeof password === "string" || password instanceof Uint8Array) return {
|
|
2415
2154
|
encryption: password,
|
|
@@ -2426,46 +2165,30 @@ function normalizePassword(password) {
|
|
|
2426
2165
|
integrity: password.integrity
|
|
2427
2166
|
};
|
|
2428
2167
|
}
|
|
2429
|
-
/** Generate cryptographically strong pseudorandom bits. */
|
|
2430
2168
|
function randomBits(bits) {
|
|
2431
2169
|
if (bits < 1) throw new Error("Invalid random bits count");
|
|
2432
|
-
|
|
2433
|
-
return randomBytes(bytes);
|
|
2170
|
+
return randomBytes(Math.ceil(bits / 8));
|
|
2434
2171
|
}
|
|
2435
|
-
/** Generates cryptographically strong pseudorandom bytes. */
|
|
2436
2172
|
function randomBytes(size) {
|
|
2437
2173
|
const bytes = new Uint8Array(size);
|
|
2438
2174
|
crypto.getRandomValues(bytes);
|
|
2439
2175
|
return bytes;
|
|
2440
2176
|
}
|
|
2441
|
-
|
|
2442
|
-
//#endregion
|
|
2443
|
-
//#region src/utils/internal/session.ts
|
|
2444
2177
|
const kGetSession = /* @__PURE__ */ Symbol.for("h3.internal.session.promise");
|
|
2445
|
-
const DEFAULT_SESSION_NAME = "h3";
|
|
2446
2178
|
const DEFAULT_SESSION_COOKIE = {
|
|
2447
2179
|
path: "/",
|
|
2448
2180
|
secure: true,
|
|
2449
2181
|
httpOnly: true
|
|
2450
2182
|
};
|
|
2451
|
-
|
|
2452
|
-
//#endregion
|
|
2453
|
-
//#region src/utils/session.ts
|
|
2454
|
-
/**
|
|
2455
|
-
* Create a session manager for the current request.
|
|
2456
|
-
*
|
|
2457
|
-
*/
|
|
2458
2183
|
async function useSession(event, config) {
|
|
2459
|
-
const sessionName = config.name ||
|
|
2184
|
+
const sessionName = config.name || "h3";
|
|
2460
2185
|
await getSession(event, config);
|
|
2461
2186
|
const sessionManager = {
|
|
2462
2187
|
get id() {
|
|
2463
|
-
|
|
2464
|
-
return context?.sessions?.[sessionName]?.id;
|
|
2188
|
+
return getEventContext(event)?.sessions?.[sessionName]?.id;
|
|
2465
2189
|
},
|
|
2466
2190
|
get data() {
|
|
2467
|
-
|
|
2468
|
-
return context.sessions?.[sessionName]?.data || {};
|
|
2191
|
+
return getEventContext(event).sessions?.[sessionName]?.data || {};
|
|
2469
2192
|
},
|
|
2470
2193
|
update: async (update) => {
|
|
2471
2194
|
await updateSession(event, config, update);
|
|
@@ -2478,11 +2201,8 @@ async function useSession(event, config) {
|
|
|
2478
2201
|
};
|
|
2479
2202
|
return sessionManager;
|
|
2480
2203
|
}
|
|
2481
|
-
/**
|
|
2482
|
-
* Get the session for the current request.
|
|
2483
|
-
*/
|
|
2484
2204
|
async function getSession(event, config) {
|
|
2485
|
-
const sessionName = config.name ||
|
|
2205
|
+
const sessionName = config.name || "h3";
|
|
2486
2206
|
const context = getEventContext(event);
|
|
2487
2207
|
if (!context.sessions) context.sessions = new EmptyObject();
|
|
2488
2208
|
const existingSession = context.sessions[sessionName];
|
|
@@ -2516,42 +2236,26 @@ async function getSession(event, config) {
|
|
|
2516
2236
|
}
|
|
2517
2237
|
return session;
|
|
2518
2238
|
}
|
|
2519
|
-
/**
|
|
2520
|
-
* Update the session data for the current request.
|
|
2521
|
-
*/
|
|
2522
2239
|
async function updateSession(event, config, update) {
|
|
2523
|
-
const sessionName = config.name ||
|
|
2524
|
-
const
|
|
2525
|
-
const session = context.sessions?.[sessionName] || await getSession(event, config);
|
|
2240
|
+
const sessionName = config.name || "h3";
|
|
2241
|
+
const session = getEventContext(event).sessions?.[sessionName] || await getSession(event, config);
|
|
2526
2242
|
if (typeof update === "function") update = update(session.data);
|
|
2527
2243
|
if (update) Object.assign(session.data, update);
|
|
2528
|
-
if (config.cookie !== false && event.res) {
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
...config.cookie
|
|
2534
|
-
});
|
|
2535
|
-
}
|
|
2244
|
+
if (config.cookie !== false && event.res) setChunkedCookie(event, sessionName, await sealSession(event, config), {
|
|
2245
|
+
...DEFAULT_SESSION_COOKIE,
|
|
2246
|
+
expires: config.maxAge ? new Date(session.createdAt + config.maxAge * 1e3) : void 0,
|
|
2247
|
+
...config.cookie
|
|
2248
|
+
});
|
|
2536
2249
|
return session;
|
|
2537
2250
|
}
|
|
2538
|
-
/**
|
|
2539
|
-
* Encrypt and sign the session data for the current request.
|
|
2540
|
-
*/
|
|
2541
2251
|
async function sealSession(event, config) {
|
|
2542
|
-
const sessionName = config.name ||
|
|
2543
|
-
|
|
2544
|
-
const session = context.sessions?.[sessionName] || await getSession(event, config);
|
|
2545
|
-
const sealed = await seal(session, config.password, {
|
|
2252
|
+
const sessionName = config.name || "h3";
|
|
2253
|
+
return await seal(getEventContext(event).sessions?.[sessionName] || await getSession(event, config), config.password, {
|
|
2546
2254
|
...defaults,
|
|
2547
2255
|
ttl: config.maxAge ? config.maxAge * 1e3 : 0,
|
|
2548
2256
|
...config.seal
|
|
2549
2257
|
});
|
|
2550
|
-
return sealed;
|
|
2551
2258
|
}
|
|
2552
|
-
/**
|
|
2553
|
-
* Decrypt and verify the session data for the current request.
|
|
2554
|
-
*/
|
|
2555
2259
|
async function unsealSession(_event, config, sealed) {
|
|
2556
2260
|
const unsealed = await unseal(sealed, config.password, {
|
|
2557
2261
|
...defaults,
|
|
@@ -2559,30 +2263,20 @@ async function unsealSession(_event, config, sealed) {
|
|
|
2559
2263
|
...config.seal
|
|
2560
2264
|
});
|
|
2561
2265
|
if (config.maxAge) {
|
|
2562
|
-
|
|
2563
|
-
if (age > config.maxAge * 1e3) throw new Error("Session expired!");
|
|
2266
|
+
if (Date.now() - (unsealed.createdAt || Number.NEGATIVE_INFINITY) > config.maxAge * 1e3) throw new Error("Session expired!");
|
|
2564
2267
|
}
|
|
2565
2268
|
return unsealed;
|
|
2566
2269
|
}
|
|
2567
|
-
/**
|
|
2568
|
-
* Clear the session data for the current request.
|
|
2569
|
-
*/
|
|
2570
2270
|
function clearSession(event, config) {
|
|
2571
2271
|
const context = getEventContext(event);
|
|
2572
|
-
const sessionName = config.name ||
|
|
2272
|
+
const sessionName = config.name || "h3";
|
|
2573
2273
|
if (context.sessions?.[sessionName]) delete context.sessions[sessionName];
|
|
2574
|
-
if (event.res && config.cookie !== false)
|
|
2274
|
+
if (event.res && config.cookie !== false) deleteChunkedCookie(event, sessionName, {
|
|
2575
2275
|
...DEFAULT_SESSION_COOKIE,
|
|
2576
2276
|
...config.cookie
|
|
2577
2277
|
});
|
|
2578
2278
|
return Promise.resolve();
|
|
2579
2279
|
}
|
|
2580
|
-
|
|
2581
|
-
//#endregion
|
|
2582
|
-
//#region src/utils/internal/cors.ts
|
|
2583
|
-
/**
|
|
2584
|
-
* Resolve CORS options.
|
|
2585
|
-
*/
|
|
2586
2280
|
function resolveCorsOptions(options = {}) {
|
|
2587
2281
|
const defaultOptions = {
|
|
2588
2282
|
origin: "*",
|
|
@@ -2593,7 +2287,7 @@ function resolveCorsOptions(options = {}) {
|
|
|
2593
2287
|
maxAge: false,
|
|
2594
2288
|
preflight: { statusCode: 204 }
|
|
2595
2289
|
};
|
|
2596
|
-
|
|
2290
|
+
const resolved = {
|
|
2597
2291
|
...defaultOptions,
|
|
2598
2292
|
...options,
|
|
2599
2293
|
preflight: {
|
|
@@ -2601,10 +2295,9 @@ function resolveCorsOptions(options = {}) {
|
|
|
2601
2295
|
...options.preflight
|
|
2602
2296
|
}
|
|
2603
2297
|
};
|
|
2298
|
+
if (resolved.credentials && (!options.origin || options.origin === "*")) console.warn("[h3] CORS: `credentials: true` with wildcard origin is not allowed. Browsers will reject the response.");
|
|
2299
|
+
return resolved;
|
|
2604
2300
|
}
|
|
2605
|
-
/**
|
|
2606
|
-
* Check if the origin is allowed.
|
|
2607
|
-
*/
|
|
2608
2301
|
function isCorsOriginAllowed(origin, options) {
|
|
2609
2302
|
const { origin: originOption } = options;
|
|
2610
2303
|
if (!origin) return false;
|
|
@@ -2616,9 +2309,6 @@ function isCorsOriginAllowed(origin, options) {
|
|
|
2616
2309
|
});
|
|
2617
2310
|
return originOption === origin;
|
|
2618
2311
|
}
|
|
2619
|
-
/**
|
|
2620
|
-
* Create the `access-control-allow-origin` header.
|
|
2621
|
-
*/
|
|
2622
2312
|
function createOriginHeaders(event, options) {
|
|
2623
2313
|
const { origin: originOption } = options;
|
|
2624
2314
|
const origin = event.req.headers.get("origin");
|
|
@@ -2633,26 +2323,17 @@ function createOriginHeaders(event, options) {
|
|
|
2633
2323
|
};
|
|
2634
2324
|
return {};
|
|
2635
2325
|
}
|
|
2636
|
-
/**
|
|
2637
|
-
* Create the `access-control-allow-methods` header.
|
|
2638
|
-
*/
|
|
2639
2326
|
function createMethodsHeaders(options) {
|
|
2640
2327
|
const { methods } = options;
|
|
2641
2328
|
if (!methods) return {};
|
|
2642
2329
|
if (methods === "*") return { "access-control-allow-methods": "*" };
|
|
2643
2330
|
return methods.length > 0 ? { "access-control-allow-methods": methods.join(",") } : {};
|
|
2644
2331
|
}
|
|
2645
|
-
/**
|
|
2646
|
-
* Create the `access-control-allow-credentials` header.
|
|
2647
|
-
*/
|
|
2648
2332
|
function createCredentialsHeaders(options) {
|
|
2649
2333
|
const { credentials } = options;
|
|
2650
2334
|
if (credentials) return { "access-control-allow-credentials": "true" };
|
|
2651
2335
|
return {};
|
|
2652
2336
|
}
|
|
2653
|
-
/**
|
|
2654
|
-
* Create the `access-control-allow-headers` and `vary` headers.
|
|
2655
|
-
*/
|
|
2656
2337
|
function createAllowHeaderHeaders(event, options) {
|
|
2657
2338
|
const { allowHeaders } = options;
|
|
2658
2339
|
if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) {
|
|
@@ -2667,37 +2348,22 @@ function createAllowHeaderHeaders(event, options) {
|
|
|
2667
2348
|
vary: "access-control-request-headers"
|
|
2668
2349
|
};
|
|
2669
2350
|
}
|
|
2670
|
-
/**
|
|
2671
|
-
* Create the `access-control-expose-headers` header.
|
|
2672
|
-
*/
|
|
2673
2351
|
function createExposeHeaders(options) {
|
|
2674
2352
|
const { exposeHeaders } = options;
|
|
2675
2353
|
if (!exposeHeaders) return {};
|
|
2676
2354
|
if (exposeHeaders === "*") return { "access-control-expose-headers": exposeHeaders };
|
|
2677
2355
|
return { "access-control-expose-headers": exposeHeaders.join(",") };
|
|
2678
2356
|
}
|
|
2679
|
-
/**
|
|
2680
|
-
* Create the `access-control-max-age` header.
|
|
2681
|
-
*/
|
|
2682
2357
|
function createMaxAgeHeader(options) {
|
|
2683
2358
|
const { maxAge } = options;
|
|
2684
2359
|
if (maxAge) return { "access-control-max-age": maxAge };
|
|
2685
2360
|
return {};
|
|
2686
2361
|
}
|
|
2687
|
-
|
|
2688
|
-
//#endregion
|
|
2689
|
-
//#region src/utils/cors.ts
|
|
2690
|
-
/**
|
|
2691
|
-
* Check if the incoming request is a CORS preflight request.
|
|
2692
|
-
*/
|
|
2693
2362
|
function isPreflightRequest(event) {
|
|
2694
2363
|
const origin = event.req.headers.get("origin");
|
|
2695
2364
|
const accessControlRequestMethod = event.req.headers.get("access-control-request-method");
|
|
2696
2365
|
return event.req.method === "OPTIONS" && !!origin && !!accessControlRequestMethod;
|
|
2697
2366
|
}
|
|
2698
|
-
/**
|
|
2699
|
-
* Append CORS preflight headers to the response.
|
|
2700
|
-
*/
|
|
2701
2367
|
function appendCorsPreflightHeaders(event, options) {
|
|
2702
2368
|
const headers = {
|
|
2703
2369
|
...createOriginHeaders(event, options),
|
|
@@ -2706,43 +2372,22 @@ function appendCorsPreflightHeaders(event, options) {
|
|
|
2706
2372
|
...createAllowHeaderHeaders(event, options),
|
|
2707
2373
|
...createMaxAgeHeader(options)
|
|
2708
2374
|
};
|
|
2709
|
-
for (const [key, value] of Object.entries(headers))
|
|
2375
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2376
|
+
event.res.headers.append(key, value);
|
|
2377
|
+
event.res.errHeaders.append(key, value);
|
|
2378
|
+
}
|
|
2710
2379
|
}
|
|
2711
|
-
/**
|
|
2712
|
-
* Append CORS headers to the response.
|
|
2713
|
-
*/
|
|
2714
2380
|
function appendCorsHeaders(event, options) {
|
|
2715
2381
|
const headers = {
|
|
2716
2382
|
...createOriginHeaders(event, options),
|
|
2717
2383
|
...createCredentialsHeaders(options),
|
|
2718
2384
|
...createExposeHeaders(options)
|
|
2719
2385
|
};
|
|
2720
|
-
for (const [key, value] of Object.entries(headers))
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
* If the incoming request is a CORS preflight request, it will append the CORS preflight headers and send a 204 response.
|
|
2726
|
-
*
|
|
2727
|
-
* If return value is not `false`, the request is handled and no further action is needed.
|
|
2728
|
-
*
|
|
2729
|
-
* @example
|
|
2730
|
-
* const app = new H3();
|
|
2731
|
-
* const router = createRouter();
|
|
2732
|
-
* router.use("/", async (event) => {
|
|
2733
|
-
* const corsRes = handleCors(event, {
|
|
2734
|
-
* origin: "*",
|
|
2735
|
-
* preflight: {
|
|
2736
|
-
* statusCode: 204,
|
|
2737
|
-
* },
|
|
2738
|
-
* methods: "*",
|
|
2739
|
-
* });
|
|
2740
|
-
* if (corsRes !== false) {
|
|
2741
|
-
* return corsRes;
|
|
2742
|
-
* }
|
|
2743
|
-
* // Your code here
|
|
2744
|
-
* });
|
|
2745
|
-
*/
|
|
2386
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2387
|
+
event.res.headers.append(key, value);
|
|
2388
|
+
event.res.errHeaders.append(key, value);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2746
2391
|
function handleCors(event, options) {
|
|
2747
2392
|
const _options = resolveCorsOptions(options);
|
|
2748
2393
|
if (isPreflightRequest(event)) {
|
|
@@ -2752,30 +2397,46 @@ function handleCors(event, options) {
|
|
|
2752
2397
|
appendCorsHeaders(event, _options);
|
|
2753
2398
|
return false;
|
|
2754
2399
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2400
|
+
const _textEncoder = /* @__PURE__ */ new TextEncoder();
|
|
2401
|
+
function timingSafeEqual(a, b) {
|
|
2402
|
+
const aBuf = _textEncoder.encode(a);
|
|
2403
|
+
const bBuf = _textEncoder.encode(b);
|
|
2404
|
+
const aLen = aBuf.length;
|
|
2405
|
+
const bLen = bBuf.length;
|
|
2406
|
+
const len = Math.max(aLen, bLen);
|
|
2407
|
+
let result = aLen === bLen ? 0 : 1;
|
|
2408
|
+
for (let i = 0; i < len; i++) result |= (aBuf[i % aLen] ?? 0) ^ (bBuf[i % bLen] ?? 0);
|
|
2409
|
+
return result === 0;
|
|
2410
|
+
}
|
|
2411
|
+
function randomJitter() {
|
|
2412
|
+
const randomBuffer = new Uint32Array(1);
|
|
2413
|
+
crypto.getRandomValues(randomBuffer);
|
|
2414
|
+
const jitter = randomBuffer[0] % 100;
|
|
2415
|
+
return new Promise((resolve) => setTimeout(resolve, jitter));
|
|
2416
|
+
}
|
|
2768
2417
|
async function requireBasicAuth(event, opts) {
|
|
2769
|
-
if (!opts.validate && !opts.password) throw new
|
|
2418
|
+
if (!opts.validate && !opts.password) throw new HTTPError({
|
|
2419
|
+
message: "Either 'password' or 'validate' option must be provided",
|
|
2420
|
+
status: 500
|
|
2421
|
+
});
|
|
2770
2422
|
const authHeader = event.req.headers.get("authorization");
|
|
2771
|
-
if (!authHeader) throw
|
|
2423
|
+
if (!authHeader) throw authFailed(event);
|
|
2772
2424
|
const [authType, b64auth] = authHeader.split(" ");
|
|
2773
|
-
if (authType !== "
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2425
|
+
if (!b64auth || authType.toLowerCase() !== "basic") throw authFailed(event, opts?.realm);
|
|
2426
|
+
let authDecoded;
|
|
2427
|
+
try {
|
|
2428
|
+
authDecoded = atob(b64auth);
|
|
2429
|
+
} catch {
|
|
2430
|
+
throw authFailed(event, opts?.realm);
|
|
2431
|
+
}
|
|
2432
|
+
const colonIndex = authDecoded.indexOf(":");
|
|
2433
|
+
const username = authDecoded.slice(0, colonIndex);
|
|
2434
|
+
const password = authDecoded.slice(colonIndex + 1);
|
|
2435
|
+
if (!username || !password) throw authFailed(event, opts?.realm);
|
|
2436
|
+
if (opts.username && !timingSafeEqual(username, opts.username) || opts.password && !timingSafeEqual(password, opts.password) || opts.validate && !await opts.validate(username, password)) {
|
|
2437
|
+
await randomJitter();
|
|
2438
|
+
throw authFailed(event, opts?.realm);
|
|
2439
|
+
}
|
|
2779
2440
|
const context = getEventContext(event);
|
|
2780
2441
|
context.basicAuth = {
|
|
2781
2442
|
username,
|
|
@@ -2784,37 +2445,19 @@ async function requireBasicAuth(event, opts) {
|
|
|
2784
2445
|
};
|
|
2785
2446
|
return true;
|
|
2786
2447
|
}
|
|
2787
|
-
/**
|
|
2788
|
-
* Create a basic authentication middleware.
|
|
2789
|
-
*
|
|
2790
|
-
* @example
|
|
2791
|
-
* import { H3, serve, basicAuth } from "h3";
|
|
2792
|
-
* const auth = basicAuth({ password: "test" });
|
|
2793
|
-
* app.get("/", (event) => `Hello ${event.context.basicAuth?.username}!`, [auth]);
|
|
2794
|
-
* serve(app, { port: 3000 });
|
|
2795
|
-
*/
|
|
2796
2448
|
function basicAuth(opts) {
|
|
2797
2449
|
return async (event, next) => {
|
|
2798
2450
|
await requireBasicAuth(event, opts);
|
|
2799
2451
|
return next();
|
|
2800
2452
|
};
|
|
2801
2453
|
}
|
|
2802
|
-
function
|
|
2454
|
+
function authFailed(event, realm = "") {
|
|
2803
2455
|
return new HTTPError({
|
|
2804
2456
|
status: 401,
|
|
2805
2457
|
statusText: "Authentication required",
|
|
2806
2458
|
headers: { "www-authenticate": `Basic realm=${JSON.stringify(realm)}` }
|
|
2807
2459
|
});
|
|
2808
2460
|
}
|
|
2809
|
-
|
|
2810
|
-
//#endregion
|
|
2811
|
-
//#region src/utils/fingerprint.ts
|
|
2812
|
-
/**
|
|
2813
|
-
*
|
|
2814
|
-
* Get a unique fingerprint for the incoming request.
|
|
2815
|
-
*
|
|
2816
|
-
* @experimental Behavior of this utility might change in the future versions
|
|
2817
|
-
*/
|
|
2818
2461
|
async function getRequestFingerprint(event, opts = {}) {
|
|
2819
2462
|
const fingerprint = [];
|
|
2820
2463
|
if (opts.ip !== false) fingerprint.push(getRequestIP(event, { xForwardedFor: opts.xForwardedFor }));
|
|
@@ -2825,77 +2468,171 @@ async function getRequestFingerprint(event, opts = {}) {
|
|
|
2825
2468
|
if (!fingerprintString) return null;
|
|
2826
2469
|
if (opts.hash === false) return fingerprintString;
|
|
2827
2470
|
const buffer = await crypto.subtle.digest(opts.hash || "SHA-1", new TextEncoder().encode(fingerprintString));
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
//#endregion
|
|
2833
|
-
//#region src/utils/ws.ts
|
|
2834
|
-
/**
|
|
2835
|
-
* Define WebSocket hooks.
|
|
2836
|
-
*
|
|
2837
|
-
* @see https://h3.dev/guide/websocket
|
|
2838
|
-
*/
|
|
2471
|
+
return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2472
|
+
}
|
|
2839
2473
|
function defineWebSocket(hooks) {
|
|
2840
2474
|
return hooks;
|
|
2841
2475
|
}
|
|
2842
|
-
/**
|
|
2843
|
-
* Define WebSocket event handler.
|
|
2844
|
-
*
|
|
2845
|
-
* @see https://h3.dev/guide/websocket
|
|
2846
|
-
*/
|
|
2847
2476
|
function defineWebSocketHandler(hooks) {
|
|
2848
|
-
return defineHandler(function _webSocketHandler() {
|
|
2849
|
-
|
|
2477
|
+
return defineHandler(function _webSocketHandler(event) {
|
|
2478
|
+
const crossws = typeof hooks === "function" ? hooks(event) : hooks;
|
|
2479
|
+
return Object.assign(new Response("WebSocket upgrade is required.", { status: 426 }), { crossws });
|
|
2850
2480
|
});
|
|
2851
2481
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2482
|
+
const PARSE_ERROR = -32700;
|
|
2483
|
+
const INVALID_REQUEST = -32600;
|
|
2484
|
+
const METHOD_NOT_FOUND = -32601;
|
|
2485
|
+
const INVALID_PARAMS = -32602;
|
|
2486
|
+
function defineJsonRpcHandler(opts = {}) {
|
|
2487
|
+
const methodMap = createMethodMap(opts.methods);
|
|
2488
|
+
const handler = async (event) => {
|
|
2489
|
+
if (event.req.method !== "POST") throw new HTTPError({ status: 405 });
|
|
2490
|
+
let body;
|
|
2491
|
+
try {
|
|
2492
|
+
body = await event.req.json();
|
|
2493
|
+
} catch {
|
|
2494
|
+
return createJsonRpcError(null, PARSE_ERROR, "Parse error");
|
|
2495
|
+
}
|
|
2496
|
+
const result = await processJsonRpcBody(body, methodMap, event);
|
|
2497
|
+
return result === void 0 ? new HTTPResponse("", { status: 202 }) : result;
|
|
2498
|
+
};
|
|
2499
|
+
return defineHandler({
|
|
2500
|
+
...opts,
|
|
2501
|
+
handler
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
function defineJsonRpcWebSocketHandler(opts) {
|
|
2505
|
+
const methodMap = createMethodMap(opts.methods);
|
|
2506
|
+
return defineWebSocketHandler({
|
|
2507
|
+
...opts.hooks,
|
|
2508
|
+
async message(peer, message) {
|
|
2509
|
+
let body;
|
|
2510
|
+
try {
|
|
2511
|
+
body = message.json();
|
|
2512
|
+
} catch {
|
|
2513
|
+
peer.send(JSON.stringify(createJsonRpcError(null, PARSE_ERROR, "Parse error")));
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
const result = await processJsonRpcBody(body, methodMap, peer);
|
|
2517
|
+
if (result !== void 0) peer.send(JSON.stringify(result));
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
function createMethodMap(methods) {
|
|
2522
|
+
const methodMap = Object.create(null);
|
|
2523
|
+
for (const key of Object.keys(methods)) methodMap[key] = methods[key];
|
|
2524
|
+
return methodMap;
|
|
2525
|
+
}
|
|
2526
|
+
async function processJsonRpcBody(body, methodMap, context) {
|
|
2527
|
+
if (!body || typeof body !== "object") return createJsonRpcError(null, PARSE_ERROR, "Parse error");
|
|
2528
|
+
const requests = Array.isArray(body) ? body : [body];
|
|
2529
|
+
if (requests.length === 0) return createJsonRpcError(null, INVALID_REQUEST, "Invalid Request");
|
|
2530
|
+
const finalResponses = (await Promise.all(requests.map((raw) => processJsonRpcMethod(raw, methodMap, context)))).filter((r) => r !== void 0);
|
|
2531
|
+
if (finalResponses.length === 0) return;
|
|
2532
|
+
return Array.isArray(body) ? finalResponses : finalResponses[0];
|
|
2533
|
+
}
|
|
2534
|
+
async function processJsonRpcMethod(raw, methodMap, context) {
|
|
2535
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return createJsonRpcError(null, INVALID_REQUEST, "Invalid Request");
|
|
2536
|
+
const req = raw;
|
|
2537
|
+
if (req.jsonrpc !== "2.0" || typeof req.method !== "string" || "id" in req && !isValidId(req.id)) return createJsonRpcError("id" in req && isValidId(req.id) ? req.id : null, INVALID_REQUEST, "Invalid Request");
|
|
2538
|
+
if ("params" in req && req.params !== void 0 && (typeof req.params !== "object" || req.params === null)) return isNotification(req) ? void 0 : createJsonRpcError(req.id, INVALID_PARAMS, "Invalid params");
|
|
2539
|
+
if (req.method.startsWith("rpc.")) return isNotification(req) ? void 0 : createJsonRpcError(req.id, METHOD_NOT_FOUND, "Method not found");
|
|
2540
|
+
const method = req.method;
|
|
2541
|
+
const params = req.params;
|
|
2542
|
+
const notification = isNotification(req);
|
|
2543
|
+
const id = notification ? void 0 : req.id;
|
|
2544
|
+
const methodHandler = methodMap[method];
|
|
2545
|
+
if (!methodHandler) return notification ? void 0 : createJsonRpcError(id, METHOD_NOT_FOUND, "Method not found");
|
|
2546
|
+
try {
|
|
2547
|
+
const rpcReq = {
|
|
2548
|
+
jsonrpc: "2.0",
|
|
2549
|
+
method,
|
|
2550
|
+
params
|
|
2551
|
+
};
|
|
2552
|
+
if (!notification) rpcReq.id = id;
|
|
2553
|
+
const result = await methodHandler(rpcReq, context);
|
|
2554
|
+
return notification ? void 0 : {
|
|
2555
|
+
jsonrpc: "2.0",
|
|
2556
|
+
id,
|
|
2557
|
+
result: result ?? null
|
|
2558
|
+
};
|
|
2559
|
+
} catch (error_) {
|
|
2560
|
+
if (notification) return;
|
|
2561
|
+
const h3Error = HTTPError.isError(error_) ? error_ : {
|
|
2562
|
+
status: 500,
|
|
2563
|
+
message: "Internal error",
|
|
2564
|
+
data: error_ != null && typeof error_ === "object" && "message" in error_ ? error_.message : void 0
|
|
2565
|
+
};
|
|
2566
|
+
const statusCode = h3Error.status;
|
|
2567
|
+
const statusMessage = h3Error.message;
|
|
2568
|
+
return createJsonRpcError(id, mapHttpStatusToJsonRpcError(statusCode), statusMessage, h3Error.data);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
function mapHttpStatusToJsonRpcError(status) {
|
|
2572
|
+
switch (status) {
|
|
2573
|
+
case 400:
|
|
2574
|
+
case 422: return INVALID_PARAMS;
|
|
2575
|
+
case 401: return -32001;
|
|
2576
|
+
case 403: return -32003;
|
|
2577
|
+
case 404: return -32004;
|
|
2578
|
+
case 408: return -32008;
|
|
2579
|
+
case 409: return -32009;
|
|
2580
|
+
case 429: return -32029;
|
|
2581
|
+
default:
|
|
2582
|
+
if (status >= 300 && status < 500) return -32e3;
|
|
2583
|
+
return -32603;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
function isNotification(req) {
|
|
2587
|
+
return !("id" in req);
|
|
2588
|
+
}
|
|
2589
|
+
function isValidId(id) {
|
|
2590
|
+
if (id === null) return true;
|
|
2591
|
+
if (typeof id === "string") return true;
|
|
2592
|
+
return typeof id === "number" && Number.isInteger(id);
|
|
2593
|
+
}
|
|
2594
|
+
const createJsonRpcError = (id, code, message, data) => {
|
|
2595
|
+
const error = {
|
|
2596
|
+
code,
|
|
2597
|
+
message
|
|
2598
|
+
};
|
|
2599
|
+
if (data !== void 0) error.data = data;
|
|
2600
|
+
return {
|
|
2601
|
+
jsonrpc: "2.0",
|
|
2602
|
+
id,
|
|
2603
|
+
error
|
|
2604
|
+
};
|
|
2605
|
+
};
|
|
2856
2606
|
const H3Error = HTTPError;
|
|
2857
2607
|
function createError(arg1, arg2) {
|
|
2858
2608
|
return new HTTPError(arg1, arg2);
|
|
2859
2609
|
}
|
|
2860
|
-
/**
|
|
2861
|
-
* @deprecated Use `HTTPError.isError`
|
|
2862
|
-
*/
|
|
2863
2610
|
function isError(input) {
|
|
2864
2611
|
return HTTPError.isError(input);
|
|
2865
2612
|
}
|
|
2866
|
-
/** @deprecated Please use `event.url` */
|
|
2867
2613
|
const getRequestPath = (event) => event.path;
|
|
2868
|
-
/** @deprecated Please use `event.req.headers.get(name)` */
|
|
2869
2614
|
function getRequestHeader(event, name) {
|
|
2870
2615
|
return event.req.headers.get(name) || void 0;
|
|
2871
2616
|
}
|
|
2872
|
-
/** @deprecated Please use `event.req.headers.get(name)` */
|
|
2873
2617
|
const getHeader = getRequestHeader;
|
|
2874
|
-
/** @deprecated Please use `Object.fromEntries(event.req.headers.entries())` */
|
|
2875
2618
|
function getRequestHeaders(event) {
|
|
2876
2619
|
return Object.fromEntries(event.req.headers.entries());
|
|
2877
2620
|
}
|
|
2878
|
-
/** @deprecated Please use `Object.fromEntries(event.req.headers.entries())` */
|
|
2879
2621
|
const getHeaders = getRequestHeaders;
|
|
2880
|
-
/** @deprecated Please use `event.req.method` */
|
|
2881
2622
|
function getMethod(event, defaultMethod = "GET") {
|
|
2882
2623
|
return (event.req.method || defaultMethod).toUpperCase();
|
|
2883
2624
|
}
|
|
2884
|
-
/** @deprecated Please use `event.req.text()` or `event.req.arrayBuffer()` */
|
|
2885
2625
|
function readRawBody(event, encoding = "utf8") {
|
|
2886
2626
|
return encoding ? event.req.text() : event.req.arrayBuffer().then((r) => new Uint8Array(r));
|
|
2887
2627
|
}
|
|
2888
|
-
/** @deprecated Please use `event.req.formData()` */
|
|
2889
2628
|
async function readFormDataBody(event) {
|
|
2890
2629
|
return event.req.formData();
|
|
2891
2630
|
}
|
|
2892
|
-
/** @deprecated Please use `event.req.formData()` */
|
|
2893
2631
|
const readFormData = readFormDataBody;
|
|
2894
|
-
/** @deprecated Please use `event.req.formData()` */
|
|
2895
2632
|
async function readMultipartFormData(event) {
|
|
2896
2633
|
const formData = await event.req.formData();
|
|
2897
2634
|
return Promise.all([...formData.entries()].map(async ([key, value]) => {
|
|
2898
|
-
return value
|
|
2635
|
+
return typeof value === "object" ? {
|
|
2899
2636
|
name: key,
|
|
2900
2637
|
type: value.type,
|
|
2901
2638
|
filename: value.name,
|
|
@@ -2906,122 +2643,82 @@ async function readMultipartFormData(event) {
|
|
|
2906
2643
|
};
|
|
2907
2644
|
}));
|
|
2908
2645
|
}
|
|
2909
|
-
/** @deprecated Please use `event.req.body` */
|
|
2910
2646
|
function getBodyStream(event) {
|
|
2911
2647
|
return event.req.body || void 0;
|
|
2912
2648
|
}
|
|
2913
|
-
/** @deprecated Please use `event.req.body` */
|
|
2914
2649
|
const getRequestWebStream = getBodyStream;
|
|
2915
|
-
/** @deprecated Please directly return stream */
|
|
2916
2650
|
function sendStream(_event, value) {
|
|
2917
2651
|
return value;
|
|
2918
2652
|
}
|
|
2919
|
-
/** @deprecated Please use `return noContent(event)` */
|
|
2920
2653
|
const sendNoContent = (_, code) => noContent(code);
|
|
2921
|
-
/** @deprecated Please use `return redirect(event, code)` */
|
|
2922
2654
|
const sendRedirect = (_, loc, code) => redirect(loc, code);
|
|
2923
|
-
/** @deprecated Please directly return response */
|
|
2924
2655
|
const sendWebResponse = (response) => response;
|
|
2925
|
-
/** @deprecated Please use `return proxy(event)` */
|
|
2926
2656
|
const sendProxy = proxy;
|
|
2927
|
-
/** @deprecated Please use `return iterable(event, value)` */
|
|
2928
2657
|
const sendIterable = (_event, val, options) => {
|
|
2929
2658
|
return iterable(val, options);
|
|
2930
2659
|
};
|
|
2931
|
-
/** @deprecated Please use `event.res.statusText` */
|
|
2932
2660
|
function getResponseStatusText(event) {
|
|
2933
2661
|
return event.res.statusText || "";
|
|
2934
2662
|
}
|
|
2935
|
-
/** @deprecated Please use `event.res.headers.append(name, value)` */
|
|
2936
2663
|
function appendResponseHeader(event, name, value) {
|
|
2937
2664
|
if (Array.isArray(value)) for (const valueItem of value) event.res.headers.append(name, valueItem);
|
|
2938
2665
|
else event.res.headers.append(name, value);
|
|
2939
2666
|
}
|
|
2940
|
-
/** @deprecated Please use `event.res.headers.append(name, value)` */
|
|
2941
2667
|
const appendHeader = appendResponseHeader;
|
|
2942
|
-
/** @deprecated Please use `event.res.headers.set(name, value)` */
|
|
2943
2668
|
function setResponseHeader(event, name, value) {
|
|
2944
2669
|
if (Array.isArray(value)) {
|
|
2945
2670
|
event.res.headers.delete(name);
|
|
2946
2671
|
for (const valueItem of value) event.res.headers.append(name, valueItem);
|
|
2947
2672
|
} else event.res.headers.set(name, value);
|
|
2948
2673
|
}
|
|
2949
|
-
/** @deprecated Please use `event.res.headers.set(name, value)` */
|
|
2950
2674
|
const setHeader = setResponseHeader;
|
|
2951
|
-
/** @deprecated Please use `event.res.headers.set(name, value)` */
|
|
2952
2675
|
function setResponseHeaders(event, headers) {
|
|
2953
2676
|
for (const [name, value] of Object.entries(headers)) event.res.headers.set(name, value);
|
|
2954
2677
|
}
|
|
2955
|
-
/** @deprecated Please use `event.res.headers.set(name, value)` */
|
|
2956
2678
|
const setHeaders = setResponseHeaders;
|
|
2957
|
-
/** @deprecated Please use `event.res.status` */
|
|
2958
2679
|
function getResponseStatus(event) {
|
|
2959
2680
|
return event.res.status || 200;
|
|
2960
2681
|
}
|
|
2961
|
-
/** @deprecated Please directly set `event.res.status` and `event.res.statusText` */
|
|
2962
2682
|
function setResponseStatus(event, code, text) {
|
|
2963
2683
|
if (code) event.res.status = sanitizeStatusCode(code, event.res.status);
|
|
2964
2684
|
if (text) event.res.statusText = sanitizeStatusMessage(text);
|
|
2965
2685
|
}
|
|
2966
|
-
/** @deprecated Please use `event.res.headers.set("content-type", type)` */
|
|
2967
2686
|
function defaultContentType(event, type) {
|
|
2968
2687
|
if (type && event.res.status !== 304 && !event.res.headers.has("content-type")) event.res.headers.set("content-type", type);
|
|
2969
2688
|
}
|
|
2970
|
-
/** @deprecated Please use `Object.fromEntries(event.res.headers.entries())` */
|
|
2971
2689
|
function getResponseHeaders(event) {
|
|
2972
2690
|
return Object.fromEntries(event.res.headers.entries());
|
|
2973
2691
|
}
|
|
2974
|
-
/** @deprecated Please use `event.res.headers.get(name)` */
|
|
2975
2692
|
function getResponseHeader(event, name) {
|
|
2976
2693
|
return event.res.headers.get(name) || void 0;
|
|
2977
2694
|
}
|
|
2978
|
-
/** @deprecated Please use `event.res.headers.delete(name)` instead. */
|
|
2979
2695
|
function removeResponseHeader(event, name) {
|
|
2980
2696
|
return event.res.headers.delete(name);
|
|
2981
2697
|
}
|
|
2982
|
-
/** @deprecated Please use `event.res.headers.append(name, value)` */
|
|
2983
2698
|
function appendResponseHeaders(event, headers) {
|
|
2984
2699
|
for (const [name, value] of Object.entries(headers)) appendResponseHeader(event, name, value);
|
|
2985
2700
|
}
|
|
2986
|
-
/** @deprecated Please use `event.res.headers.append(name, value)` */
|
|
2987
2701
|
const appendHeaders = appendResponseHeaders;
|
|
2988
|
-
/** @deprecated Please use `event.res.headers.delete` */
|
|
2989
2702
|
function clearResponseHeaders(event, headerNames) {
|
|
2990
2703
|
if (headerNames && headerNames.length > 0) for (const name of headerNames) event.res.headers.delete(name);
|
|
2991
2704
|
else for (const name of event.res.headers.keys()) event.res.headers.delete(name);
|
|
2992
2705
|
}
|
|
2993
|
-
/** Please use `defineHandler` */
|
|
2994
2706
|
const defineEventHandler = defineHandler;
|
|
2995
|
-
/** Please use `defineHandler` */
|
|
2996
2707
|
const eventHandler = defineHandler;
|
|
2997
|
-
/** Please use `defineLazyEventHandler` */
|
|
2998
2708
|
const lazyEventHandler = defineLazyEventHandler;
|
|
2999
|
-
/** @deprecated Please use `defineNodeHandler` */
|
|
3000
2709
|
const defineNodeListener = defineNodeHandler;
|
|
3001
|
-
/** @deprecated Please use `defineNodeHandler` */
|
|
3002
2710
|
const fromNodeMiddleware = fromNodeHandler;
|
|
3003
|
-
/**
|
|
3004
|
-
* @deprecated please use `toNodeHandler` from `h3/node`.
|
|
3005
|
-
*/
|
|
3006
2711
|
function toNodeHandler(app) {
|
|
3007
2712
|
if (toNodeHandler._isWarned !== true) {
|
|
3008
2713
|
console.warn(`[h3] "toNodeHandler" export from h3 is deprecated. Please import "toNodeHandler" from "h3/node".`);
|
|
3009
2714
|
toNodeHandler._isWarned = true;
|
|
3010
2715
|
}
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
})();
|
|
3015
|
-
return _toNodeHandler(app.fetch);
|
|
2716
|
+
return (toNodeHandler._toNodeHandler ??= () => {
|
|
2717
|
+
return globalThis.process.getBuiltinModule("node:module").createRequire(import.meta.url)("srvx/node").toNodeHandler;
|
|
2718
|
+
})()(app.fetch);
|
|
3016
2719
|
}
|
|
3017
|
-
/** @deprecated Please use `toNodeHandler` */
|
|
3018
2720
|
const toNodeListener = toNodeHandler;
|
|
3019
|
-
/** @deprecated Please use `new H3()` */
|
|
3020
2721
|
const createApp = (config) => new H3(config);
|
|
3021
|
-
/** @deprecated Please use `new H3()` */
|
|
3022
2722
|
const createRouter$1 = (config) => new H3(config);
|
|
3023
|
-
/** @deprecated Please use `withBase()` */
|
|
3024
2723
|
const useBase = withBase;
|
|
3025
|
-
|
|
3026
|
-
//#endregion
|
|
3027
|
-
export { H3, H3Core, H3Error, H3Event, HTTPError, HTTPResponse, appendCorsHeaders, appendCorsPreflightHeaders, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, basicAuth, callMiddleware, clearResponseHeaders, clearSession, createApp, createError, createEventStream, createRouter$1 as createRouter, defaultContentType, defineEventHandler, defineHandler, defineLazyEventHandler, defineMiddleware, defineNodeHandler, defineNodeListener, defineNodeMiddleware, definePlugin, defineRoute, defineValidatedHandler, defineWebSocket, defineWebSocketHandler, deleteChunkedCookie, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, freezeApp, fromNodeHandler, fromNodeMiddleware, fromWebHandler, getBodyStream, getChunkedCookie, getCookie, getEventContext, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestFingerprint, getRequestHeader, getRequestHeaders, getRequestHost, getRequestIP, getRequestPath, getRequestProtocol, getRequestURL, getRequestWebStream, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, getValidatedQuery, getValidatedRouterParams, handleCacheHeaders, handleCors, html, isCorsOriginAllowed, isError, isEvent, isHTTPEvent, isMethod, isPreflightRequest, iterable, lazyEventHandler, mockEvent, noContent, onError, onRequest, onResponse, parseCookies, proxy, proxyRequest, readBody, readFormData, readFormDataBody, readMultipartFormData, readRawBody, readValidatedBody, redirect, removeResponseHeader, requireBasicAuth, sanitizeStatusCode, sanitizeStatusMessage, sealSession, sendIterable, sendNoContent, sendProxy, sendRedirect, sendStream, sendWebResponse, serveStatic, setChunkedCookie, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, toEventHandler, toNodeHandler, toNodeListener, toRequest, toResponse, toWebHandler, unsealSession, updateSession, useBase, useSession, withBase, writeEarlyHints };
|
|
2724
|
+
export { basicAuth as $, defineLazyEventHandler as $t, readFormDataBody as A, freezeApp as An, bodyLimit as At, setHeader as B, redirect as Bt, getResponseHeader as C, toMiddleware as Cn, parseCookies as Ct, isError as D, sanitizeStatusCode as Dn, getProxyRequestHeaders as Dt, getResponseStatusText as E, HTTPError as En, fetchWithEvent as Et, sendNoContent as F, readBody as Ft, toNodeHandler as G, defineNodeHandler as Gt, setResponseHeader as H, writeEarlyHints as Ht, sendProxy as I, readValidatedBody as It, defineJsonRpcHandler as J, fromWebHandler as Jt, toNodeListener as K, defineNodeMiddleware as Kt, sendRedirect as L, html as Lt, readRawBody as M, onRequest as Mt, removeResponseHeader as N, onResponse as Nt, lazyEventHandler as O, sanitizeStatusMessage as On, proxy as Ot, sendIterable as P, assertBodySize as Pt, getRequestFingerprint as Q, defineHandler as Qt, sendStream as R, iterable as Rt, getRequestWebStream as S, defineMiddleware as Sn, getValidatedCookies as St, getResponseStatus as T, toResponse as Tn, setCookie as Tt, setResponseHeaders as U, defineRoute as Ut, setHeaders as V, redirectBack as Vt, setResponseStatus as W, removeRoute$1 as Wt, defineWebSocket as X, H3 as Xt, defineJsonRpcWebSocketHandler as Y, toWebHandler as Yt, defineWebSocketHandler as Z, H3Core as Zt, getHeaders as _, getEventContext as _n, createEventStream as _t, appendResponseHeaders as a, getRequestHost as an, isCorsOriginAllowed as at, getRequestHeaders as b, mockEvent as bn, getChunkedCookie as bt, createError as c, getRequestURL as cn, sealSession as ct, defineEventHandler as d, getValidatedQuery as dn, useSession as dt, defineValidatedHandler as en, requireBasicAuth as et, defineNodeListener as f, getValidatedRouterParams as fn, withBase as ft, getHeader as g, toRequest as gn, withServerTiming as gt, getBodyStream as h, requestWithURL as hn, setServerTiming as ht, appendResponseHeader as i, getQuery as in, isPreflightRequest as it, readMultipartFormData as j, onError as jt, readFormData as k, H3Event as kn, proxyRequest as kt, createRouter$1 as l, getRouterParam as ln, unsealSession as lt, fromNodeMiddleware as m, requestWithBaseURL as mn, handleCacheHeaders as mt, appendHeader as n, toEventHandler as nn, appendCorsPreflightHeaders as nt, clearResponseHeaders as o, getRequestIP as on, clearSession as ot, eventHandler as p, isMethod as pn, serveStatic as pt, useBase as q, fromNodeHandler as qt, appendHeaders as r, assertMethod as rn, handleCors as rt, createApp as s, getRequestProtocol as sn, getSession as st, H3Error as t, dynamicEventHandler as tn, appendCorsHeaders as tt, defaultContentType as u, getRouterParams as un, updateSession as ut, getMethod as v, isEvent as vn, deleteChunkedCookie as vt, getResponseHeaders as w, HTTPResponse as wn, setChunkedCookie as wt, getRequestPath as x, callMiddleware as xn, getCookie as xt, getRequestHeader as y, isHTTPEvent as yn, deleteCookie as yt, sendWebResponse as z, noContent as zt };
|