@depup/h3 2.0.1-depup.0
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/LICENSE +21 -0
- package/README.md +25 -0
- package/bin/h3.mjs +36 -0
- package/changes.json +5 -0
- package/dist/THIRD-PARTY-LICENSES.md +70 -0
- package/dist/_entries/bun.d.mts +6 -0
- package/dist/_entries/bun.mjs +16 -0
- package/dist/_entries/cloudflare.d.mts +6 -0
- package/dist/_entries/cloudflare.mjs +16 -0
- package/dist/_entries/deno.d.mts +6 -0
- package/dist/_entries/deno.mjs +16 -0
- package/dist/_entries/generic.d.mts +6 -0
- package/dist/_entries/generic.mjs +16 -0
- package/dist/_entries/node.d.mts +10 -0
- package/dist/_entries/node.mjs +19 -0
- package/dist/_entries/service-worker.d.mts +6 -0
- package/dist/_entries/service-worker.mjs +16 -0
- package/dist/_utils.mjs +240 -0
- package/dist/cache.mjs +599 -0
- package/dist/cache2.mjs +50 -0
- package/dist/cors.mjs +292 -0
- 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 +167 -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 +171 -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.rules.md +698 -0
- package/dist/docs/0.guide/3.api/0.h3.md +144 -0
- package/dist/docs/0.guide/3.api/1.h3event.md +160 -0
- package/dist/docs/0.guide/4.advanced/0.plugins.md +50 -0
- package/dist/docs/0.guide/4.advanced/1.websocket.md +176 -0
- package/dist/docs/0.guide/4.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 +447 -0
- package/dist/docs/1.utils/2.response.md +172 -0
- package/dist/docs/1.utils/3.cookie.md +33 -0
- package/dist/docs/1.utils/4.security.md +175 -0
- package/dist/docs/1.utils/5.proxy.md +57 -0
- package/dist/docs/1.utils/6.mcp.md +75 -0
- package/dist/docs/1.utils/7.more.md +117 -0
- package/dist/docs/1.utils/8.community.md +48 -0
- package/dist/docs/2.examples/0.index/index.md +17 -0
- package/dist/docs/2.examples/1.handle-cookie.md +67 -0
- package/dist/docs/2.examples/2.handle-query.md +76 -0
- package/dist/docs/2.examples/3.handle-session.md +210 -0
- package/dist/docs/2.examples/4.serve-static-assets.md +66 -0
- package/dist/docs/2.examples/5.stream-response.md +76 -0
- package/dist/docs/2.examples/6.validate-data.md +193 -0
- package/dist/docs/3.migration/0.index/index.md +204 -0
- package/dist/docs/README.md +37 -0
- package/dist/h3.d.mts +1669 -0
- package/dist/h3.mjs +1809 -0
- package/dist/index.d.mts +1634 -0
- package/dist/match.d.mts +123 -0
- package/dist/middleware.mjs +123 -0
- package/dist/normalize.mjs +645 -0
- package/dist/path.mjs +42 -0
- package/dist/proxy.mjs +254 -0
- package/dist/response.mjs +465 -0
- package/dist/rules/cache.d.mts +29 -0
- package/dist/rules/cache.mjs +163 -0
- package/dist/rules/compiler.d.mts +94 -0
- package/dist/rules/compiler.mjs +173 -0
- package/dist/rules/index.d.mts +77 -0
- package/dist/rules/index.mjs +34 -0
- package/dist/rules/proxy.d.mts +3 -0
- package/dist/rules/proxy.mjs +14 -0
- package/dist/tracing.d.mts +33 -0
- package/dist/tracing.mjs +89 -0
- package/package.json +148 -0
package/dist/h3.mjs
ADDED
|
@@ -0,0 +1,1809 @@
|
|
|
1
|
+
import { EmptyObject, HTTPError, HTTPResponse, decodePreservingSeparators, hasEmptySegmentAfterBase, hasProp, kHandled, normalizeRoute, onDispose, sanitizeStatusCode, sanitizeStatusMessage, toResponse, withoutBase, withoutTrailingSlash } from "./response.mjs";
|
|
2
|
+
import "./middleware.mjs";
|
|
3
|
+
import { H3, defineHandler, defineLazyEventHandler, defineValidatedHandler, getEventContext, getRequestIP, isCacheMatch, toEventHandler, validateData } from "./cache.mjs";
|
|
4
|
+
import { base64Decode, base64Encode, isCorsOriginAllowed, iterable, noContent, redirect, textDecoder, textEncoder } from "./cors.mjs";
|
|
5
|
+
import { proxy } from "./proxy.mjs";
|
|
6
|
+
import { isCanonicalPath } from "./path.mjs";
|
|
7
|
+
import { addRoute, createRouter, removeRoute } from "rou3";
|
|
8
|
+
import { limitRequestBody } from "srvx/body-limit";
|
|
9
|
+
function freezeApp(app) {
|
|
10
|
+
app.config = Object.freeze(app.config);
|
|
11
|
+
app["~addRoute"] = () => {
|
|
12
|
+
throw new Error("Cannot add routes after the server init.");
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function definePlugin(def) {
|
|
16
|
+
return ((opts) => (h3) => def(h3, opts));
|
|
17
|
+
}
|
|
18
|
+
function toWebHandler(app) {
|
|
19
|
+
return (request, context) => {
|
|
20
|
+
return Promise.resolve(app.request(request, void 0, context || request.context));
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function fromWebHandler(handler) {
|
|
24
|
+
return function _webHandler(event) {
|
|
25
|
+
return handler(event.req, event.context);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function fromNodeHandler(handler) {
|
|
29
|
+
if (typeof handler !== "function") throw new TypeError(`Invalid handler. It should be a function: ${handler}`);
|
|
30
|
+
return function _nodeHandler(event) {
|
|
31
|
+
const node = event.runtime?.node;
|
|
32
|
+
if (!node?.res) throw new Error("[h3] Executing Node.js middleware is not supported in this server!");
|
|
33
|
+
const url = event.url.pathname + event.url.search;
|
|
34
|
+
if (node.req.url === url) return callNodeHandler(handler, node.req, node.res);
|
|
35
|
+
const originalUrl = node.req.url;
|
|
36
|
+
node.req.url = url;
|
|
37
|
+
return callNodeHandler(handler, node.req, node.res).finally(() => {
|
|
38
|
+
node.req.url = originalUrl;
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function defineNodeHandler(handler) {
|
|
43
|
+
return handler;
|
|
44
|
+
}
|
|
45
|
+
function defineNodeMiddleware(handler) {
|
|
46
|
+
return handler;
|
|
47
|
+
}
|
|
48
|
+
function callNodeHandler(handler, req, res) {
|
|
49
|
+
const isMiddleware = handler.length > 2;
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
res.once("close", () => resolve(kHandled));
|
|
52
|
+
res.once("finish", () => resolve(kHandled));
|
|
53
|
+
res.once("error", (error) => reject(error));
|
|
54
|
+
res.once("pipe", (stream) => {
|
|
55
|
+
resolve(new Promise((resolve, reject) => {
|
|
56
|
+
const onResClose = () => {
|
|
57
|
+
stream.destroy();
|
|
58
|
+
resolve(kHandled);
|
|
59
|
+
};
|
|
60
|
+
const settle = (cb) => {
|
|
61
|
+
res.removeListener("close", onResClose);
|
|
62
|
+
cb();
|
|
63
|
+
};
|
|
64
|
+
stream.once("close", () => settle(() => resolve(kHandled)));
|
|
65
|
+
stream.once("error", (error) => settle(() => {
|
|
66
|
+
console.error("[h3] Stream error in Node.js handler", { cause: error });
|
|
67
|
+
reject(kHandled);
|
|
68
|
+
}));
|
|
69
|
+
if (res.closed || res.destroyed) onResClose();
|
|
70
|
+
else res.once("close", onResClose);
|
|
71
|
+
}));
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
if (isMiddleware) Promise.resolve(handler(req, res, (error) => error ? reject(new HTTPError({
|
|
75
|
+
cause: error,
|
|
76
|
+
unhandled: true
|
|
77
|
+
})) : resolve(void 0))).catch((error) => reject(new HTTPError({
|
|
78
|
+
cause: error,
|
|
79
|
+
unhandled: true
|
|
80
|
+
})));
|
|
81
|
+
else return Promise.resolve(handler(req, res)).then(() => resolve(kHandled)).catch((error) => reject(new HTTPError({
|
|
82
|
+
cause: error,
|
|
83
|
+
unhandled: true
|
|
84
|
+
})));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
reject(new HTTPError({
|
|
87
|
+
cause: error,
|
|
88
|
+
unhandled: true
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function defineRoute(def) {
|
|
94
|
+
const handler = defineValidatedHandler(def);
|
|
95
|
+
return (h3) => {
|
|
96
|
+
h3.on(def.method, def.route, handler);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function removeRoute$1(app, method, route) {
|
|
100
|
+
const _method = method ? method.toUpperCase() : "";
|
|
101
|
+
route = normalizeRoute(route);
|
|
102
|
+
const routes = app["~routes"];
|
|
103
|
+
const kept = routes.filter((r) => !(r.route === route && (r.method || "") === _method));
|
|
104
|
+
if (kept.length === routes.length) {
|
|
105
|
+
removeRoute(app["~rou3"], _method, route);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
app["~routes"] = kept;
|
|
109
|
+
const rou3 = app["~rou3"];
|
|
110
|
+
if (rou3) {
|
|
111
|
+
const rebuilt = createRouter();
|
|
112
|
+
for (const r of kept) addRoute(rebuilt, r.method || "", r.route, r);
|
|
113
|
+
rou3.root = rebuilt.root;
|
|
114
|
+
rou3.static = rebuilt.static;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function appendAcceptQuery(event, mediaTypes) {
|
|
118
|
+
const list = Array.isArray(mediaTypes) ? mediaTypes : [mediaTypes];
|
|
119
|
+
if (list.length === 0) return;
|
|
120
|
+
const value = list.map(serializeMediaType).join(", ");
|
|
121
|
+
event.res.headers.append("accept-query", value);
|
|
122
|
+
}
|
|
123
|
+
function requireContentType(event, acceptedTypes) {
|
|
124
|
+
const header = event.req.headers.get("content-type");
|
|
125
|
+
if (!header) throw new HTTPError({
|
|
126
|
+
status: 400,
|
|
127
|
+
statusText: "Bad Request",
|
|
128
|
+
message: "Content-Type header is required"
|
|
129
|
+
});
|
|
130
|
+
const mediaType = header.split(";")[0].trim().toLowerCase();
|
|
131
|
+
const slash = mediaType.indexOf("/");
|
|
132
|
+
if (slash <= 0 || slash === mediaType.length - 1) throw new HTTPError({
|
|
133
|
+
status: 422,
|
|
134
|
+
statusText: "Unprocessable Content",
|
|
135
|
+
message: "Malformed Content-Type header"
|
|
136
|
+
});
|
|
137
|
+
const accepted = Array.isArray(acceptedTypes) ? acceptedTypes : [acceptedTypes];
|
|
138
|
+
if (accepted.some((type) => mediaTypeMatches(mediaType, type.split(";")[0].trim().toLowerCase()))) return mediaType;
|
|
139
|
+
throw new HTTPError({
|
|
140
|
+
status: 415,
|
|
141
|
+
statusText: "Unsupported Media Type",
|
|
142
|
+
message: `Unsupported Content-Type: ${mediaType}. Expected one of: ${accepted.join(", ")}`
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const SF_TOKEN_RE = /^[A-Za-z*][\w!#$%&'*+.^`|~:/-]*$/;
|
|
146
|
+
const SF_KEY_RE = /^[a-z*][a-z0-9_.*-]*$/;
|
|
147
|
+
function serializeMediaType(mediaType) {
|
|
148
|
+
const parts = splitOutsideQuotes(mediaType, ";");
|
|
149
|
+
const base = parts[0].trim();
|
|
150
|
+
if (!SF_TOKEN_RE.test(base)) throw new TypeError(`Invalid media type: ${JSON.stringify(mediaType)}`);
|
|
151
|
+
let result = base;
|
|
152
|
+
for (let i = 1; i < parts.length; i++) {
|
|
153
|
+
const param = parts[i].trim();
|
|
154
|
+
if (!param) continue;
|
|
155
|
+
const eq = param.indexOf("=");
|
|
156
|
+
const key = (eq === -1 ? param : param.slice(0, eq)).trim().toLowerCase();
|
|
157
|
+
if (!SF_KEY_RE.test(key)) throw new TypeError(`Invalid media type parameter: ${JSON.stringify(param)}`);
|
|
158
|
+
result += eq === -1 ? `;${key}` : `;${key}="${escapeQuotes(unquote(param.slice(eq + 1).trim()))}"`;
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function mediaTypeMatches(mediaType, accepted) {
|
|
163
|
+
if (accepted === "*/*" || accepted === "*") return true;
|
|
164
|
+
if (accepted === mediaType) return true;
|
|
165
|
+
if (accepted.endsWith("/*")) return mediaType.startsWith(accepted.slice(0, -1));
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function splitOutsideQuotes(input, sep) {
|
|
169
|
+
const parts = [];
|
|
170
|
+
let current = "";
|
|
171
|
+
let inQuotes = false;
|
|
172
|
+
for (let i = 0; i < input.length; i++) {
|
|
173
|
+
const ch = input[i];
|
|
174
|
+
if (inQuotes) {
|
|
175
|
+
current += ch;
|
|
176
|
+
if (ch === "\\" && i + 1 < input.length) current += input[++i];
|
|
177
|
+
else if (ch === "\"") inQuotes = false;
|
|
178
|
+
} else if (ch === "\"") {
|
|
179
|
+
inQuotes = true;
|
|
180
|
+
current += ch;
|
|
181
|
+
} else if (ch === sep) {
|
|
182
|
+
parts.push(current);
|
|
183
|
+
current = "";
|
|
184
|
+
} else current += ch;
|
|
185
|
+
}
|
|
186
|
+
parts.push(current);
|
|
187
|
+
return parts;
|
|
188
|
+
}
|
|
189
|
+
function escapeQuotes(value) {
|
|
190
|
+
return value.replace(/[\\"]/g, "\\$&");
|
|
191
|
+
}
|
|
192
|
+
function unquote(value) {
|
|
193
|
+
if (value.length >= 2 && value[0] === "\"" && value.endsWith("\"")) return value.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function parseURLEncodedBody(body) {
|
|
197
|
+
return collectEntries(new URLSearchParams(body).entries());
|
|
198
|
+
}
|
|
199
|
+
function parseFormData(form) {
|
|
200
|
+
return collectEntries(form.entries());
|
|
201
|
+
}
|
|
202
|
+
function collectEntries(entries) {
|
|
203
|
+
const parsed = new EmptyObject();
|
|
204
|
+
for (const [key, value] of entries) if (hasProp(parsed, key)) {
|
|
205
|
+
if (!Array.isArray(parsed[key])) parsed[key] = [parsed[key]];
|
|
206
|
+
parsed[key].push(value);
|
|
207
|
+
} else parsed[key] = value;
|
|
208
|
+
return parsed;
|
|
209
|
+
}
|
|
210
|
+
async function readBody(event, options) {
|
|
211
|
+
const contentType = event.req.headers.get("content-type") || "";
|
|
212
|
+
const type = options?.type;
|
|
213
|
+
if (type === "formData") {
|
|
214
|
+
let form;
|
|
215
|
+
try {
|
|
216
|
+
form = await event.req.formData();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (HTTPError.isError(error)) throw error;
|
|
219
|
+
throw new HTTPError({
|
|
220
|
+
status: 400,
|
|
221
|
+
statusText: "Bad Request",
|
|
222
|
+
message: "Invalid form data body"
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return parseFormData(form);
|
|
226
|
+
}
|
|
227
|
+
const text = await event.req.text();
|
|
228
|
+
if (type === "text") return text;
|
|
229
|
+
if (!text) return;
|
|
230
|
+
if (type === "urlencoded" || !type && contentType.startsWith("application/x-www-form-urlencoded")) return parseURLEncodedBody(text);
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(text);
|
|
233
|
+
} catch {
|
|
234
|
+
throw new HTTPError({
|
|
235
|
+
status: 400,
|
|
236
|
+
statusText: "Bad Request",
|
|
237
|
+
message: "Invalid JSON body"
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function readValidatedBody(event, validate, options) {
|
|
242
|
+
const _body = await readBody(event, options);
|
|
243
|
+
return validateData(_body, validate, options);
|
|
244
|
+
}
|
|
245
|
+
function assertBodySize(event, limit) {
|
|
246
|
+
const req = event.req;
|
|
247
|
+
if (!req.body) return;
|
|
248
|
+
const contentLength = req.headers.get("content-length");
|
|
249
|
+
if (contentLength) {
|
|
250
|
+
if (req.headers.get("transfer-encoding")) throw new HTTPError({ status: 400 });
|
|
251
|
+
if (+contentLength > limit) throw bodyTooLargeError(limit);
|
|
252
|
+
}
|
|
253
|
+
event.req = limitRequestBody(req, limit, { createError: () => bodyTooLargeError(limit) });
|
|
254
|
+
}
|
|
255
|
+
function bodyTooLargeError(limit) {
|
|
256
|
+
return new HTTPError({
|
|
257
|
+
status: 413,
|
|
258
|
+
statusText: "Request Entity Too Large",
|
|
259
|
+
message: `Request body size exceeds the limit of ${limit} bytes`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function onRequest(hook) {
|
|
263
|
+
return async function _onRequestMiddleware(event) {
|
|
264
|
+
await hook(event);
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function onResponse(hook) {
|
|
268
|
+
return async function _onResponseMiddleware(event, next) {
|
|
269
|
+
const rawBody = await next();
|
|
270
|
+
const response = await toResponse(rawBody, event);
|
|
271
|
+
return await hook(response, event) || response;
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function onError(hook) {
|
|
275
|
+
return async (event, next) => {
|
|
276
|
+
try {
|
|
277
|
+
return await next();
|
|
278
|
+
} catch (rawError) {
|
|
279
|
+
const isHTTPError = HTTPError.isError(rawError);
|
|
280
|
+
const error = isHTTPError ? rawError : new HTTPError(rawError);
|
|
281
|
+
if (!isHTTPError) {
|
|
282
|
+
error.unhandled = true;
|
|
283
|
+
if (rawError?.stack) error.stack = rawError.stack;
|
|
284
|
+
}
|
|
285
|
+
const hookResponse = await hook(error, event);
|
|
286
|
+
if (hookResponse !== void 0) return hookResponse;
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function bodyLimit(limit) {
|
|
292
|
+
return (event, next) => {
|
|
293
|
+
assertBodySize(event, limit);
|
|
294
|
+
return next();
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const COOKIE_MAX_AGE_LIMIT = 3456e4;
|
|
298
|
+
function endIndex(str, min, len) {
|
|
299
|
+
const index = str.indexOf(";", min);
|
|
300
|
+
return index === -1 ? len : index;
|
|
301
|
+
}
|
|
302
|
+
function eqIndex(str, min, max) {
|
|
303
|
+
const index = str.indexOf("=", min);
|
|
304
|
+
return index < max ? index : -1;
|
|
305
|
+
}
|
|
306
|
+
function valueSlice(str, min, max) {
|
|
307
|
+
if (min === max) return "";
|
|
308
|
+
let start = min;
|
|
309
|
+
let end = max;
|
|
310
|
+
do {
|
|
311
|
+
const code = str.charCodeAt(start);
|
|
312
|
+
if (code !== 32 && code !== 9) break;
|
|
313
|
+
} while (++start < end);
|
|
314
|
+
while (end > start) {
|
|
315
|
+
const code = str.charCodeAt(end - 1);
|
|
316
|
+
if (code !== 32 && code !== 9) break;
|
|
317
|
+
end--;
|
|
318
|
+
}
|
|
319
|
+
return str.slice(start, end);
|
|
320
|
+
}
|
|
321
|
+
const NullObject = /* @__PURE__ */ (() => {
|
|
322
|
+
const C = function() {};
|
|
323
|
+
C.prototype = Object.create(null);
|
|
324
|
+
return C;
|
|
325
|
+
})();
|
|
326
|
+
function parse(str, options) {
|
|
327
|
+
const obj = new NullObject();
|
|
328
|
+
const len = str.length;
|
|
329
|
+
if (len < 2) return obj;
|
|
330
|
+
const dec = options?.decode || decode;
|
|
331
|
+
const allowMultiple = options?.allowMultiple || false;
|
|
332
|
+
let index = 0;
|
|
333
|
+
do {
|
|
334
|
+
const eqIdx = eqIndex(str, index, len);
|
|
335
|
+
if (eqIdx === -1) break;
|
|
336
|
+
const endIdx = endIndex(str, index, len);
|
|
337
|
+
if (eqIdx > endIdx) {
|
|
338
|
+
index = str.lastIndexOf(";", eqIdx - 1) + 1;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const key = valueSlice(str, index, eqIdx);
|
|
342
|
+
if (options?.filter && !options.filter(key)) {
|
|
343
|
+
index = endIdx + 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const val = dec(valueSlice(str, eqIdx + 1, endIdx));
|
|
347
|
+
if (allowMultiple) {
|
|
348
|
+
const existing = obj[key];
|
|
349
|
+
if (existing === void 0) obj[key] = val;
|
|
350
|
+
else if (Array.isArray(existing)) existing.push(val);
|
|
351
|
+
else obj[key] = [existing, val];
|
|
352
|
+
} else if (obj[key] === void 0) obj[key] = val;
|
|
353
|
+
index = endIdx + 1;
|
|
354
|
+
} while (index < len);
|
|
355
|
+
return obj;
|
|
356
|
+
}
|
|
357
|
+
function decode(str) {
|
|
358
|
+
if (!str.includes("%")) return str;
|
|
359
|
+
try {
|
|
360
|
+
return decodeURIComponent(str);
|
|
361
|
+
} catch {
|
|
362
|
+
return str;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const cookieNameRegExp = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/;
|
|
366
|
+
const cookieValueRegExp = /^[\u0021-\u003A\u003C-\u007E]*$/;
|
|
367
|
+
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;
|
|
368
|
+
const pathValueRegExp = /^[\u0020-\u003A\u003C-\u007E]*$/;
|
|
369
|
+
const __toString = Object.prototype.toString;
|
|
370
|
+
function serialize(_a0, _a1, _a2) {
|
|
371
|
+
const isObj = typeof _a0 === "object" && _a0 !== null;
|
|
372
|
+
const options = isObj ? _a1 : _a2;
|
|
373
|
+
const stringify = options?.stringify || JSON.stringify;
|
|
374
|
+
const cookie = isObj ? _a0 : {
|
|
375
|
+
..._a2,
|
|
376
|
+
name: _a0,
|
|
377
|
+
value: _a1 == void 0 ? "" : typeof _a1 === "string" ? _a1 : stringify(_a1)
|
|
378
|
+
};
|
|
379
|
+
const enc = options?.encode || encodeURIComponent;
|
|
380
|
+
if (!cookieNameRegExp.test(cookie.name)) throw new TypeError(`argument name is invalid: ${cookie.name}`);
|
|
381
|
+
const value = cookie.value ? enc(cookie.value) : "";
|
|
382
|
+
if (!cookieValueRegExp.test(value)) throw new TypeError(`argument val is invalid: ${cookie.value}`);
|
|
383
|
+
if (!cookie.secure) {
|
|
384
|
+
if (cookie.partitioned) throw new TypeError(`Partitioned cookies must have the Secure attribute`);
|
|
385
|
+
if (cookie.sameSite && String(cookie.sameSite).toLowerCase() === "none") throw new TypeError(`SameSite=None cookies must have the Secure attribute`);
|
|
386
|
+
if (cookie.name.length > 9 && cookie.name.charCodeAt(0) === 95 && cookie.name.charCodeAt(1) === 95) {
|
|
387
|
+
const nameLower = cookie.name.toLowerCase();
|
|
388
|
+
if (nameLower.startsWith("__secure-") || nameLower.startsWith("__host-")) throw new TypeError(`${cookie.name} cookies must have the Secure attribute`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (cookie.name.length > 7 && cookie.name.charCodeAt(0) === 95 && cookie.name.charCodeAt(1) === 95 && cookie.name.toLowerCase().startsWith("__host-")) {
|
|
392
|
+
if (cookie.path !== "/") throw new TypeError(`__Host- cookies must have Path=/`);
|
|
393
|
+
if (cookie.domain) throw new TypeError(`__Host- cookies must not have a Domain attribute`);
|
|
394
|
+
}
|
|
395
|
+
let str = cookie.name + "=" + value;
|
|
396
|
+
if (cookie.maxAge !== void 0) {
|
|
397
|
+
if (!Number.isInteger(cookie.maxAge)) throw new TypeError(`option maxAge is invalid: ${cookie.maxAge}`);
|
|
398
|
+
str += "; Max-Age=" + Math.max(0, Math.min(cookie.maxAge, COOKIE_MAX_AGE_LIMIT));
|
|
399
|
+
}
|
|
400
|
+
if (cookie.domain) {
|
|
401
|
+
if (!domainValueRegExp.test(cookie.domain)) throw new TypeError(`option domain is invalid: ${cookie.domain}`);
|
|
402
|
+
str += "; Domain=" + cookie.domain;
|
|
403
|
+
}
|
|
404
|
+
if (cookie.path) {
|
|
405
|
+
if (!pathValueRegExp.test(cookie.path)) throw new TypeError(`option path is invalid: ${cookie.path}`);
|
|
406
|
+
str += "; Path=" + cookie.path;
|
|
407
|
+
}
|
|
408
|
+
if (cookie.expires) {
|
|
409
|
+
if (!isDate(cookie.expires) || !Number.isFinite(cookie.expires.valueOf())) throw new TypeError(`option expires is invalid: ${cookie.expires}`);
|
|
410
|
+
str += "; Expires=" + cookie.expires.toUTCString();
|
|
411
|
+
}
|
|
412
|
+
if (cookie.httpOnly) str += "; HttpOnly";
|
|
413
|
+
if (cookie.secure) str += "; Secure";
|
|
414
|
+
if (cookie.partitioned) str += "; Partitioned";
|
|
415
|
+
if (cookie.priority) switch (typeof cookie.priority === "string" ? cookie.priority.toLowerCase() : void 0) {
|
|
416
|
+
case "low":
|
|
417
|
+
str += "; Priority=Low";
|
|
418
|
+
break;
|
|
419
|
+
case "medium":
|
|
420
|
+
str += "; Priority=Medium";
|
|
421
|
+
break;
|
|
422
|
+
case "high":
|
|
423
|
+
str += "; Priority=High";
|
|
424
|
+
break;
|
|
425
|
+
default: throw new TypeError(`option priority is invalid: ${cookie.priority}`);
|
|
426
|
+
}
|
|
427
|
+
if (cookie.sameSite) switch (typeof cookie.sameSite === "string" ? cookie.sameSite.toLowerCase() : cookie.sameSite) {
|
|
428
|
+
case true:
|
|
429
|
+
case "strict":
|
|
430
|
+
str += "; SameSite=Strict";
|
|
431
|
+
break;
|
|
432
|
+
case "lax":
|
|
433
|
+
str += "; SameSite=Lax";
|
|
434
|
+
break;
|
|
435
|
+
case "none":
|
|
436
|
+
str += "; SameSite=None";
|
|
437
|
+
break;
|
|
438
|
+
default: throw new TypeError(`option sameSite is invalid: ${cookie.sameSite}`);
|
|
439
|
+
}
|
|
440
|
+
return str;
|
|
441
|
+
}
|
|
442
|
+
function isDate(val) {
|
|
443
|
+
return __toString.call(val) === "[object Date]";
|
|
444
|
+
}
|
|
445
|
+
const maxAgeRegExp = /^-?\d+$/;
|
|
446
|
+
const _nullProto = /* @__PURE__ */ Object.getPrototypeOf({});
|
|
447
|
+
function parseSetCookie(str, options) {
|
|
448
|
+
const len = str.length;
|
|
449
|
+
let _endIdx = len;
|
|
450
|
+
let eqIdx = -1;
|
|
451
|
+
for (let i = 0; i < len; i++) {
|
|
452
|
+
const c = str.charCodeAt(i);
|
|
453
|
+
if (c === 59) {
|
|
454
|
+
_endIdx = i;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
if (c === 61 && eqIdx === -1) eqIdx = i;
|
|
458
|
+
}
|
|
459
|
+
if (eqIdx >= _endIdx) eqIdx = -1;
|
|
460
|
+
const name = eqIdx === -1 ? "" : _trim(str, 0, eqIdx);
|
|
461
|
+
if (name && name in _nullProto) return void 0;
|
|
462
|
+
let value = eqIdx === -1 ? _trim(str, 0, _endIdx) : _trim(str, eqIdx + 1, _endIdx);
|
|
463
|
+
if (!name && !value) return void 0;
|
|
464
|
+
if (name.length + value.length > 4096) return void 0;
|
|
465
|
+
if (options?.decode !== false) value = _decode(value, options?.decode);
|
|
466
|
+
const setCookie = {
|
|
467
|
+
name,
|
|
468
|
+
value
|
|
469
|
+
};
|
|
470
|
+
let index = _endIdx + 1;
|
|
471
|
+
while (index < len) {
|
|
472
|
+
let endIdx = len;
|
|
473
|
+
let attrEqIdx = -1;
|
|
474
|
+
for (let i = index; i < len; i++) {
|
|
475
|
+
const c = str.charCodeAt(i);
|
|
476
|
+
if (c === 59) {
|
|
477
|
+
endIdx = i;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
if (c === 61 && attrEqIdx === -1) attrEqIdx = i;
|
|
481
|
+
}
|
|
482
|
+
if (attrEqIdx >= endIdx) attrEqIdx = -1;
|
|
483
|
+
const attr = attrEqIdx === -1 ? _trim(str, index, endIdx) : _trim(str, index, attrEqIdx);
|
|
484
|
+
const val = attrEqIdx === -1 ? void 0 : _trim(str, attrEqIdx + 1, endIdx);
|
|
485
|
+
if (val === void 0 || val.length <= 1024) switch (attr.toLowerCase()) {
|
|
486
|
+
case "httponly":
|
|
487
|
+
setCookie.httpOnly = true;
|
|
488
|
+
break;
|
|
489
|
+
case "secure":
|
|
490
|
+
setCookie.secure = true;
|
|
491
|
+
break;
|
|
492
|
+
case "partitioned":
|
|
493
|
+
setCookie.partitioned = true;
|
|
494
|
+
break;
|
|
495
|
+
case "domain":
|
|
496
|
+
if (val) setCookie.domain = (val.charCodeAt(0) === 46 ? val.slice(1) : val).toLowerCase();
|
|
497
|
+
break;
|
|
498
|
+
case "path":
|
|
499
|
+
setCookie.path = val;
|
|
500
|
+
break;
|
|
501
|
+
case "max-age":
|
|
502
|
+
if (val && maxAgeRegExp.test(val)) setCookie.maxAge = Math.min(Number(val), COOKIE_MAX_AGE_LIMIT);
|
|
503
|
+
break;
|
|
504
|
+
case "expires": {
|
|
505
|
+
if (!val) break;
|
|
506
|
+
const date = new Date(val);
|
|
507
|
+
if (Number.isFinite(date.valueOf())) {
|
|
508
|
+
const maxDate = new Date(Date.now() + COOKIE_MAX_AGE_LIMIT * 1e3);
|
|
509
|
+
setCookie.expires = date > maxDate ? maxDate : date;
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case "priority": {
|
|
514
|
+
if (!val) break;
|
|
515
|
+
const priority = val.toLowerCase();
|
|
516
|
+
if (priority === "low" || priority === "medium" || priority === "high") setCookie.priority = priority;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "samesite": {
|
|
520
|
+
if (!val) break;
|
|
521
|
+
const sameSite = val.toLowerCase();
|
|
522
|
+
if (sameSite === "lax" || sameSite === "strict" || sameSite === "none") setCookie.sameSite = sameSite;
|
|
523
|
+
else setCookie.sameSite = "lax";
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
default: {
|
|
527
|
+
const attrLower = attr.toLowerCase();
|
|
528
|
+
if (attrLower && !(attrLower in _nullProto)) setCookie[attrLower] = val;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
index = endIdx + 1;
|
|
532
|
+
}
|
|
533
|
+
return setCookie;
|
|
534
|
+
}
|
|
535
|
+
function _trim(str, start, end) {
|
|
536
|
+
if (start === end) return "";
|
|
537
|
+
let s = start;
|
|
538
|
+
let e = end;
|
|
539
|
+
while (s < e && (str.charCodeAt(s) === 32 || str.charCodeAt(s) === 9)) s++;
|
|
540
|
+
while (e > s && (str.charCodeAt(e - 1) === 32 || str.charCodeAt(e - 1) === 9)) e--;
|
|
541
|
+
return str.slice(s, e);
|
|
542
|
+
}
|
|
543
|
+
function _decode(value, decode) {
|
|
544
|
+
if (!decode && !value.includes("%")) return value;
|
|
545
|
+
try {
|
|
546
|
+
return (decode || decodeURIComponent)(value);
|
|
547
|
+
} catch {
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const CHUNKED_COOKIE = "__chunked__";
|
|
552
|
+
const CHUNKS_MAX_LENGTH = 4e3;
|
|
553
|
+
function parseCookies(event) {
|
|
554
|
+
return parse(event.req.headers.get("cookie") || "");
|
|
555
|
+
}
|
|
556
|
+
function getValidatedCookies(event, validate, options) {
|
|
557
|
+
const cookies = parseCookies(event);
|
|
558
|
+
return validateData(cookies, validate, options);
|
|
559
|
+
}
|
|
560
|
+
function getCookie(event, name) {
|
|
561
|
+
return parseCookies(event)[name];
|
|
562
|
+
}
|
|
563
|
+
function setCookie(event, name, value, options) {
|
|
564
|
+
const { encode, stringify, ...attrs } = options ?? {};
|
|
565
|
+
const newCookie = serialize({
|
|
566
|
+
name,
|
|
567
|
+
value,
|
|
568
|
+
path: "/",
|
|
569
|
+
...attrs
|
|
570
|
+
}, {
|
|
571
|
+
encode,
|
|
572
|
+
stringify
|
|
573
|
+
});
|
|
574
|
+
const currentCookies = event.res.headers.getSetCookie();
|
|
575
|
+
if (currentCookies.length === 0) {
|
|
576
|
+
event.res.headers.set("set-cookie", newCookie);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const namePrefix = `${name}=`;
|
|
580
|
+
if (!currentCookies.some((cookie) => cookie.startsWith(namePrefix))) {
|
|
581
|
+
event.res.headers.append("set-cookie", newCookie);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const newCookieKey = _getDistinctCookieKey(name, options || {});
|
|
585
|
+
event.res.headers.delete("set-cookie");
|
|
586
|
+
for (const cookie of currentCookies) {
|
|
587
|
+
const parsed = parseSetCookie(cookie);
|
|
588
|
+
if (parsed ? _getDistinctCookieKey(cookie.split("=")?.[0], parsed) === newCookieKey : cookie.startsWith(namePrefix)) continue;
|
|
589
|
+
event.res.headers.append("set-cookie", cookie);
|
|
590
|
+
}
|
|
591
|
+
event.res.headers.append("set-cookie", newCookie);
|
|
592
|
+
}
|
|
593
|
+
function deleteCookie(event, name, serializeOptions) {
|
|
594
|
+
setCookie(event, name, "", {
|
|
595
|
+
...serializeOptions,
|
|
596
|
+
maxAge: 0
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function getChunkedCookie(event, name) {
|
|
600
|
+
const cookies = parseCookies(event);
|
|
601
|
+
const mainCookie = cookies[name];
|
|
602
|
+
if (!mainCookie || !mainCookie.startsWith(CHUNKED_COOKIE)) return mainCookie;
|
|
603
|
+
const chunksCount = getChunkedCookieCount(mainCookie);
|
|
604
|
+
if (chunksCount === 0) return;
|
|
605
|
+
const chunks = [];
|
|
606
|
+
for (let i = 1; i <= chunksCount; i++) {
|
|
607
|
+
const chunk = cookies[chunkCookieName(name, i)];
|
|
608
|
+
if (!chunk) return;
|
|
609
|
+
chunks.push(chunk);
|
|
610
|
+
}
|
|
611
|
+
return chunks.join("");
|
|
612
|
+
}
|
|
613
|
+
function setChunkedCookie(event, name, value, options) {
|
|
614
|
+
const chunkMaxLength = options?.chunkMaxLength || CHUNKS_MAX_LENGTH;
|
|
615
|
+
const chunks = splitCookieValue(value, chunkMaxLength, options?.encode || encodeURIComponent);
|
|
616
|
+
if (!chunks) throw new HTTPError({
|
|
617
|
+
status: 500,
|
|
618
|
+
message: `Cannot set chunked cookie "${name}": a single character does not fit in chunkMaxLength (${chunkMaxLength}) once encoded.`
|
|
619
|
+
});
|
|
620
|
+
const chunkCount = chunks.length;
|
|
621
|
+
if (chunkCount > MAX_CHUNKED_COOKIE_COUNT) throw new HTTPError({
|
|
622
|
+
status: 500,
|
|
623
|
+
message: `Cannot set chunked cookie "${name}": value needs more than ${MAX_CHUNKED_COOKIE_COUNT} chunks.`
|
|
624
|
+
});
|
|
625
|
+
const previousCookie = getCookie(event, name);
|
|
626
|
+
if (previousCookie?.startsWith(CHUNKED_COOKIE)) {
|
|
627
|
+
const previousChunkCount = getChunkedCookieCount(previousCookie);
|
|
628
|
+
const newChunkCount = chunkCount <= 1 ? 0 : chunkCount;
|
|
629
|
+
for (let i = newChunkCount + 1; i <= previousChunkCount; i++) deleteCookie(event, chunkCookieName(name, i), options);
|
|
630
|
+
}
|
|
631
|
+
if (chunkCount <= 1) {
|
|
632
|
+
setCookie(event, name, value, options);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
setCookie(event, name, `${CHUNKED_COOKIE}${chunkCount}`, options);
|
|
636
|
+
for (let i = 1; i <= chunkCount; i++) setCookie(event, chunkCookieName(name, i), chunks[i - 1], options);
|
|
637
|
+
}
|
|
638
|
+
function deleteChunkedCookie(event, name, serializeOptions) {
|
|
639
|
+
const mainCookie = getCookie(event, name);
|
|
640
|
+
deleteCookie(event, name, serializeOptions);
|
|
641
|
+
const chunksCount = getChunkedCookieCount(mainCookie);
|
|
642
|
+
if (chunksCount >= 0) for (let i = 0; i < chunksCount; i++) deleteCookie(event, chunkCookieName(name, i + 1), serializeOptions);
|
|
643
|
+
}
|
|
644
|
+
function _getDistinctCookieKey(name, options) {
|
|
645
|
+
return [
|
|
646
|
+
name,
|
|
647
|
+
(options.domain || "").replace(/^\./, "").toLowerCase(),
|
|
648
|
+
options.path || "/",
|
|
649
|
+
options.partitioned ? "1" : "0"
|
|
650
|
+
].join(";");
|
|
651
|
+
}
|
|
652
|
+
const MAX_CHUNKED_COOKIE_COUNT = 100;
|
|
653
|
+
function getChunkedCookieCount(cookie) {
|
|
654
|
+
if (!cookie?.startsWith(CHUNKED_COOKIE)) return NaN;
|
|
655
|
+
const count = Number.parseInt(cookie.slice(11));
|
|
656
|
+
if (Number.isNaN(count) || count < 0 || count > MAX_CHUNKED_COOKIE_COUNT) return NaN;
|
|
657
|
+
return count;
|
|
658
|
+
}
|
|
659
|
+
function splitCookieValue(value, maxLength, encode) {
|
|
660
|
+
const chars = Array.from(value);
|
|
661
|
+
const slice = (start, end) => chars.slice(start, end).join("");
|
|
662
|
+
const fits = (start, end) => encode(slice(start, end)).length <= maxLength;
|
|
663
|
+
const chunks = [];
|
|
664
|
+
let start = 0;
|
|
665
|
+
while (start < chars.length) {
|
|
666
|
+
if (!fits(start, start + 1)) return;
|
|
667
|
+
let lo = start + 1;
|
|
668
|
+
let hi = chars.length + 1;
|
|
669
|
+
let probe = Math.min(start + maxLength, chars.length);
|
|
670
|
+
while (probe > lo) {
|
|
671
|
+
if (!fits(start, probe)) {
|
|
672
|
+
hi = probe;
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
lo = probe;
|
|
676
|
+
probe = Math.min(start + (probe - start) * 2, chars.length);
|
|
677
|
+
}
|
|
678
|
+
while (hi - lo > 1) {
|
|
679
|
+
const mid = lo + hi >>> 1;
|
|
680
|
+
if (fits(start, mid)) lo = mid;
|
|
681
|
+
else hi = mid;
|
|
682
|
+
}
|
|
683
|
+
chunks.push(slice(start, lo));
|
|
684
|
+
if (chunks.length > MAX_CHUNKED_COOKIE_COUNT) return chunks;
|
|
685
|
+
start = lo;
|
|
686
|
+
}
|
|
687
|
+
return chunks;
|
|
688
|
+
}
|
|
689
|
+
function chunkCookieName(name, chunkNumber) {
|
|
690
|
+
return `${name}.${chunkNumber}`;
|
|
691
|
+
}
|
|
692
|
+
function formatEventStreamComment(comment) {
|
|
693
|
+
return comment.split(/\r\n|\r|\n/).map((l) => `: ${l}\n`).join("") + "\n";
|
|
694
|
+
}
|
|
695
|
+
function formatEventStreamMessage(message) {
|
|
696
|
+
let result = "";
|
|
697
|
+
if (message.id) result += `id: ${_sanitizeSingleLine(message.id)}\n`;
|
|
698
|
+
if (message.event) result += `event: ${_sanitizeSingleLine(message.event)}\n`;
|
|
699
|
+
if (typeof message.retry === "number" && Number.isInteger(message.retry)) result += `retry: ${message.retry}\n`;
|
|
700
|
+
const data = typeof message.data === "string" ? message.data : "";
|
|
701
|
+
for (const line of data.split(/\r\n|\r|\n/)) result += `data: ${line}\n`;
|
|
702
|
+
result += "\n";
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
function _sanitizeSingleLine(value) {
|
|
706
|
+
return value.replace(/[\n\r]/g, "");
|
|
707
|
+
}
|
|
708
|
+
function formatEventStreamMessages(messages) {
|
|
709
|
+
let result = "";
|
|
710
|
+
for (const msg of messages) result += formatEventStreamMessage(msg);
|
|
711
|
+
return result;
|
|
712
|
+
}
|
|
713
|
+
function eventStreamHeaders(event) {
|
|
714
|
+
const headers = {
|
|
715
|
+
"content-type": "text/event-stream",
|
|
716
|
+
"cache-control": "private, no-cache, no-store, no-transform, must-revalidate, max-age=0",
|
|
717
|
+
"x-accel-buffering": "no"
|
|
718
|
+
};
|
|
719
|
+
if (event.req.headers.get("connection") === "keep-alive") headers["connection"] = "keep-alive";
|
|
720
|
+
return headers;
|
|
721
|
+
}
|
|
722
|
+
function setEventStreamHeaders(event) {
|
|
723
|
+
for (const [name, value] of Object.entries(eventStreamHeaders(event))) event.res.headers.set(name, value);
|
|
724
|
+
}
|
|
725
|
+
const _noop = () => {};
|
|
726
|
+
var EventStream = class extends HTTPResponse {
|
|
727
|
+
_event;
|
|
728
|
+
_transformStream;
|
|
729
|
+
_writer;
|
|
730
|
+
_encoder = new TextEncoder();
|
|
731
|
+
_closeCallbacks = [];
|
|
732
|
+
_writerIsClosed = false;
|
|
733
|
+
_paused = false;
|
|
734
|
+
_unsentData;
|
|
735
|
+
_disposed = false;
|
|
736
|
+
_closing;
|
|
737
|
+
get _isClosed() {
|
|
738
|
+
return this._writerIsClosed || this._disposed;
|
|
739
|
+
}
|
|
740
|
+
constructor(event, _opts = {}) {
|
|
741
|
+
const transformStream = new TransformStream();
|
|
742
|
+
super(transformStream.readable, {
|
|
743
|
+
status: 200,
|
|
744
|
+
headers: eventStreamHeaders(event)
|
|
745
|
+
});
|
|
746
|
+
this._event = event;
|
|
747
|
+
this._transformStream = transformStream;
|
|
748
|
+
this._writer = transformStream.writable.getWriter();
|
|
749
|
+
this._writer.closed.catch(_noop).finally(() => {
|
|
750
|
+
this._writerIsClosed = true;
|
|
751
|
+
this._disposed = true;
|
|
752
|
+
for (const cb of this._closeCallbacks.splice(0)) _invokeCloseCallback(cb);
|
|
753
|
+
});
|
|
754
|
+
onDispose(this._event, () => {
|
|
755
|
+
if (!this._isClosed && !this._transformStream.readable.locked) return this._transformStream.readable.cancel().catch(_noop);
|
|
756
|
+
return this.close();
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
async push(message) {
|
|
760
|
+
if (typeof message === "string") {
|
|
761
|
+
await this._sendEvent({ data: message });
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (Array.isArray(message)) {
|
|
765
|
+
if (message.length === 0) return;
|
|
766
|
+
if (typeof message[0] === "string") {
|
|
767
|
+
const msgs = [];
|
|
768
|
+
for (const item of message) msgs.push({ data: item });
|
|
769
|
+
await this._sendEvents(msgs);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
await this._sendEvents(message);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
await this._sendEvent(message);
|
|
776
|
+
}
|
|
777
|
+
async pushComment(comment) {
|
|
778
|
+
if (this._isClosed) return;
|
|
779
|
+
if (this._paused && !this._unsentData) {
|
|
780
|
+
this._unsentData = formatEventStreamComment(comment);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (this._paused) {
|
|
784
|
+
this._unsentData += formatEventStreamComment(comment);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
await this._writer.write(this._encoder.encode(formatEventStreamComment(comment))).catch(() => {
|
|
788
|
+
this._writerIsClosed = true;
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
async _sendEvent(message) {
|
|
792
|
+
if (this._isClosed) return;
|
|
793
|
+
if (this._paused && !this._unsentData) {
|
|
794
|
+
this._unsentData = formatEventStreamMessage(message);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (this._paused) {
|
|
798
|
+
this._unsentData += formatEventStreamMessage(message);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
await this._writer.write(this._encoder.encode(formatEventStreamMessage(message))).catch(() => {
|
|
802
|
+
this._writerIsClosed = true;
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
async _sendEvents(messages) {
|
|
806
|
+
if (this._isClosed) return;
|
|
807
|
+
const payload = formatEventStreamMessages(messages);
|
|
808
|
+
if (this._paused && !this._unsentData) {
|
|
809
|
+
this._unsentData = payload;
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (this._paused) {
|
|
813
|
+
this._unsentData += payload;
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
await this._writer.write(this._encoder.encode(payload)).catch(() => {
|
|
817
|
+
this._writerIsClosed = true;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
pause() {
|
|
821
|
+
this._paused = true;
|
|
822
|
+
}
|
|
823
|
+
get isPaused() {
|
|
824
|
+
return this._paused;
|
|
825
|
+
}
|
|
826
|
+
async resume() {
|
|
827
|
+
this._paused = false;
|
|
828
|
+
await this.flush();
|
|
829
|
+
}
|
|
830
|
+
async flush() {
|
|
831
|
+
if (this._isClosed) return;
|
|
832
|
+
if (this._unsentData?.length) {
|
|
833
|
+
const data = this._unsentData;
|
|
834
|
+
this._unsentData = void 0;
|
|
835
|
+
await this._writer.write(this._encoder.encode(data)).catch(() => {
|
|
836
|
+
this._writerIsClosed = true;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
close() {
|
|
841
|
+
return this._closing ??= this._close();
|
|
842
|
+
}
|
|
843
|
+
async _close() {
|
|
844
|
+
if (this._disposed) return;
|
|
845
|
+
if (!this._isClosed) {
|
|
846
|
+
do {
|
|
847
|
+
this._paused = false;
|
|
848
|
+
await this.flush();
|
|
849
|
+
} while (!this._isClosed && this._unsentData?.length);
|
|
850
|
+
try {
|
|
851
|
+
await this._writer.close();
|
|
852
|
+
} catch {}
|
|
853
|
+
}
|
|
854
|
+
this._disposed = true;
|
|
855
|
+
}
|
|
856
|
+
onClosed(cb) {
|
|
857
|
+
if (this._writerIsClosed) {
|
|
858
|
+
queueMicrotask(() => _invokeCloseCallback(cb));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
this._closeCallbacks.push(cb);
|
|
862
|
+
}
|
|
863
|
+
async send() {
|
|
864
|
+
setEventStreamHeaders(this._event);
|
|
865
|
+
this._event.res.status = 200;
|
|
866
|
+
return this._transformStream.readable;
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
function _invokeCloseCallback(cb) {
|
|
870
|
+
try {
|
|
871
|
+
const res = cb();
|
|
872
|
+
if (res instanceof Promise) res.catch(_noop);
|
|
873
|
+
} catch {}
|
|
874
|
+
}
|
|
875
|
+
function setServerTiming(event, name, opts) {
|
|
876
|
+
if (!_isValidToken(name)) throw new TypeError(`Invalid Server-Timing metric name: ${name}`);
|
|
877
|
+
if (opts?.dur !== void 0 && (!Number.isFinite(opts.dur) || opts.dur < 0)) throw new TypeError(`Invalid Server-Timing duration: ${opts.dur}`);
|
|
878
|
+
const value = name + (opts?.desc ? `;desc="${_escapeDesc(opts.desc)}"` : "") + (opts?.dur !== void 0 ? `;dur=${opts.dur}` : "");
|
|
879
|
+
event.res.headers.append("server-timing", value);
|
|
880
|
+
const ctx = event.context;
|
|
881
|
+
if (!Array.isArray(ctx.timing)) ctx.timing = [];
|
|
882
|
+
ctx.timing.push({
|
|
883
|
+
name,
|
|
884
|
+
...opts
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async function withServerTiming(event, name, fn) {
|
|
888
|
+
const start = performance.now();
|
|
889
|
+
try {
|
|
890
|
+
return await fn();
|
|
891
|
+
} finally {
|
|
892
|
+
setServerTiming(event, name, { dur: performance.now() - start });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const _tokenRE = /^[\w!#$%&'*+.^`|~-]+$/;
|
|
896
|
+
function _isValidToken(value) {
|
|
897
|
+
return _tokenRE.test(value);
|
|
898
|
+
}
|
|
899
|
+
function _escapeDesc(value) {
|
|
900
|
+
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
901
|
+
}
|
|
902
|
+
const COMMON_MIME_TYPES = {
|
|
903
|
+
".html": "text/html",
|
|
904
|
+
".htm": "text/html",
|
|
905
|
+
".css": "text/css",
|
|
906
|
+
".js": "text/javascript",
|
|
907
|
+
".mjs": "text/javascript",
|
|
908
|
+
".cjs": "text/javascript",
|
|
909
|
+
".json": "application/json",
|
|
910
|
+
".txt": "text/plain",
|
|
911
|
+
".xml": "application/xml",
|
|
912
|
+
".gif": "image/gif",
|
|
913
|
+
".ico": "image/vnd.microsoft.icon",
|
|
914
|
+
".jpeg": "image/jpeg",
|
|
915
|
+
".jpg": "image/jpeg",
|
|
916
|
+
".png": "image/png",
|
|
917
|
+
".svg": "image/svg+xml",
|
|
918
|
+
".webp": "image/webp",
|
|
919
|
+
".woff": "font/woff",
|
|
920
|
+
".woff2": "font/woff2",
|
|
921
|
+
".mp4": "video/mp4",
|
|
922
|
+
".webm": "video/webm",
|
|
923
|
+
".zip": "application/zip",
|
|
924
|
+
".pdf": "application/pdf"
|
|
925
|
+
};
|
|
926
|
+
function getExtension(path) {
|
|
927
|
+
const filename = path.split("/").pop();
|
|
928
|
+
if (!filename) return;
|
|
929
|
+
const separatorIndex = filename.lastIndexOf(".");
|
|
930
|
+
if (separatorIndex !== -1) return filename.slice(separatorIndex);
|
|
931
|
+
}
|
|
932
|
+
function getType(ext) {
|
|
933
|
+
return ext ? COMMON_MIME_TYPES[ext] : void 0;
|
|
934
|
+
}
|
|
935
|
+
async function serveStatic(event, options) {
|
|
936
|
+
if (options.headers) {
|
|
937
|
+
const entries = Array.isArray(options.headers) ? options.headers : typeof options.headers.entries === "function" ? options.headers.entries() : Object.entries(options.headers);
|
|
938
|
+
for (const [key, value] of entries) event.res.headers.set(key, value);
|
|
939
|
+
}
|
|
940
|
+
if (event.req.method !== "GET" && event.req.method !== "HEAD") {
|
|
941
|
+
if (options.fallthrough) return;
|
|
942
|
+
event.res.headers.set("allow", "GET, HEAD");
|
|
943
|
+
throw new HTTPError({ status: 405 });
|
|
944
|
+
}
|
|
945
|
+
if (!isCanonicalPath(event.url.pathname)) {
|
|
946
|
+
if (options.fallthrough) return;
|
|
947
|
+
throw new HTTPError({ status: 404 });
|
|
948
|
+
}
|
|
949
|
+
const resolvedId = withoutTrailingSlash(event.url.pathname);
|
|
950
|
+
let originalId = resolvedId;
|
|
951
|
+
if (resolvedId.includes("%")) try {
|
|
952
|
+
const decodedId = decodePreservingSeparators(resolvedId, {
|
|
953
|
+
decode: decodeURI,
|
|
954
|
+
nested: false
|
|
955
|
+
});
|
|
956
|
+
if (isCanonicalPath(decodedId)) originalId = withoutTrailingSlash(decodedId);
|
|
957
|
+
} catch {}
|
|
958
|
+
const acceptEncodings = parseAcceptEncoding(event.req.headers.get("accept-encoding") || "", options.encodings);
|
|
959
|
+
let id = originalId;
|
|
960
|
+
let assetId = originalId;
|
|
961
|
+
let meta;
|
|
962
|
+
const _ids = idSearchPaths(originalId, acceptEncodings, options.indexNames || ["/index.html"]);
|
|
963
|
+
for (const _id of _ids) {
|
|
964
|
+
const _meta = await options.getMeta(_id.id);
|
|
965
|
+
if (_meta) {
|
|
966
|
+
meta = _meta;
|
|
967
|
+
id = _id.id;
|
|
968
|
+
assetId = _id.assetId;
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (!meta) {
|
|
973
|
+
if (options.fallthrough) return;
|
|
974
|
+
throw new HTTPError({ statusCode: 404 });
|
|
975
|
+
}
|
|
976
|
+
if (options.encodings && Object.keys(options.encodings).length > 0) {
|
|
977
|
+
const vary = event.res.headers.get("vary") || "";
|
|
978
|
+
if (!/(?:^|,)\s*(?:\*|accept-encoding)\s*(?:,|$)/i.test(vary)) event.res.headers.append("vary", "accept-encoding");
|
|
979
|
+
}
|
|
980
|
+
let mtimeDate;
|
|
981
|
+
if (meta.mtime) {
|
|
982
|
+
mtimeDate = new Date(meta.mtime);
|
|
983
|
+
mtimeDate.setMilliseconds(0);
|
|
984
|
+
if (!event.res.headers.get("last-modified")) event.res.headers.set("last-modified", mtimeDate.toUTCString());
|
|
985
|
+
}
|
|
986
|
+
if (meta.etag && !event.res.headers.has("etag")) event.res.headers.set("etag", meta.etag);
|
|
987
|
+
if (isCacheMatch(event.req.headers, {
|
|
988
|
+
etag: meta.etag,
|
|
989
|
+
lastModified: mtimeDate
|
|
990
|
+
})) return new HTTPResponse(null, {
|
|
991
|
+
status: 304,
|
|
992
|
+
statusText: "Not Modified"
|
|
993
|
+
});
|
|
994
|
+
if (!event.res.headers.get("content-type")) {
|
|
995
|
+
if (meta.type) event.res.headers.set("content-type", meta.type);
|
|
996
|
+
else {
|
|
997
|
+
const ext = getExtension(assetId);
|
|
998
|
+
const type = ext ? options.getType?.(ext) ?? getType(ext) : void 0;
|
|
999
|
+
if (type) event.res.headers.set("content-type", type);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (meta.encoding && !event.res.headers.get("content-encoding")) event.res.headers.set("content-encoding", meta.encoding);
|
|
1003
|
+
if (meta.size !== void 0 && meta.size > 0 && !event.res.headers.get("content-length")) event.res.headers.set("content-length", meta.size + "");
|
|
1004
|
+
if (event.req.method === "HEAD") return new HTTPResponse(null, { status: 200 });
|
|
1005
|
+
const contents = await options.getContents(id);
|
|
1006
|
+
return new HTTPResponse(contents || null, { status: 200 });
|
|
1007
|
+
}
|
|
1008
|
+
const WEIGHT_RE = /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/;
|
|
1009
|
+
function parseAcceptEncoding(header, encodingMap) {
|
|
1010
|
+
if (!encodingMap || !header) return [];
|
|
1011
|
+
const extensions = /* @__PURE__ */ new Map();
|
|
1012
|
+
for (const [name, ext] of Object.entries(encodingMap)) extensions.set(normalizeCoding(name), ext);
|
|
1013
|
+
const weights = /* @__PURE__ */ new Map();
|
|
1014
|
+
let wildcard;
|
|
1015
|
+
for (const part of header.split(",")) {
|
|
1016
|
+
const [rawName = "", ...params] = part.split(";");
|
|
1017
|
+
const name = normalizeCoding(rawName);
|
|
1018
|
+
if (name !== "*" && !extensions.has(name)) continue;
|
|
1019
|
+
const qParam = params.find((p) => /^\s*q\s*=/i.test(p));
|
|
1020
|
+
const qValue = qParam?.slice(qParam.indexOf("=") + 1).trim();
|
|
1021
|
+
const q = qValue === void 0 ? 1 : WEIGHT_RE.test(qValue) ? Number(qValue) : 0;
|
|
1022
|
+
if (name === "*") wildcard ??= q;
|
|
1023
|
+
else if (!weights.has(name)) weights.set(name, q);
|
|
1024
|
+
}
|
|
1025
|
+
if (wildcard) {
|
|
1026
|
+
for (const name of extensions.keys()) if (!weights.has(name)) weights.set(name, wildcard);
|
|
1027
|
+
}
|
|
1028
|
+
const accepted = [...weights].filter(([, q]) => q > 0).sort((a, b) => b[1] - a[1]).map(([name]) => extensions.get(name));
|
|
1029
|
+
return [...new Set(accepted)];
|
|
1030
|
+
}
|
|
1031
|
+
function normalizeCoding(name) {
|
|
1032
|
+
const coding = name.trim().toLowerCase();
|
|
1033
|
+
return coding === "x-gzip" || coding === "x-compress" ? coding.slice(2) : coding;
|
|
1034
|
+
}
|
|
1035
|
+
function idSearchPaths(id, encodings, indexNames) {
|
|
1036
|
+
const ids = [];
|
|
1037
|
+
for (const suffix of ["", ...indexNames]) {
|
|
1038
|
+
const assetId = `${id}${suffix}`;
|
|
1039
|
+
for (const encoding of [...encodings, ""]) ids.push({
|
|
1040
|
+
id: `${assetId}${encoding}`,
|
|
1041
|
+
assetId
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
return ids;
|
|
1045
|
+
}
|
|
1046
|
+
function withBase(base, input) {
|
|
1047
|
+
base = withoutTrailingSlash(base);
|
|
1048
|
+
const handler = toEventHandler(input);
|
|
1049
|
+
if (!handler) throw new Error("Invalid handler", { cause: input });
|
|
1050
|
+
return async function _handlerWithBase(event) {
|
|
1051
|
+
const _pathBefore = event.url.pathname || "/";
|
|
1052
|
+
if (base !== "/" && hasEmptySegmentAfterBase(_pathBefore, base)) throw new HTTPError({ status: 404 });
|
|
1053
|
+
event.url.pathname = withoutBase(event.url.pathname || "/", base);
|
|
1054
|
+
try {
|
|
1055
|
+
return await handler(event);
|
|
1056
|
+
} finally {
|
|
1057
|
+
event.url.pathname = _pathBefore;
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
const defaults = /* @__PURE__ */ Object.freeze({
|
|
1062
|
+
ttl: 0,
|
|
1063
|
+
timestampSkewSec: 60,
|
|
1064
|
+
localtimeOffsetMsec: 0,
|
|
1065
|
+
encryption: /* @__PURE__ */ Object.freeze({
|
|
1066
|
+
saltBits: 256,
|
|
1067
|
+
algorithm: "aes-256-cbc",
|
|
1068
|
+
iterations: 8192,
|
|
1069
|
+
minPasswordlength: 32
|
|
1070
|
+
}),
|
|
1071
|
+
integrity: /* @__PURE__ */ Object.freeze({
|
|
1072
|
+
saltBits: 256,
|
|
1073
|
+
algorithm: "sha256",
|
|
1074
|
+
iterations: 8192,
|
|
1075
|
+
minPasswordlength: 32
|
|
1076
|
+
})
|
|
1077
|
+
});
|
|
1078
|
+
const algorithms = /* @__PURE__ */ Object.freeze({
|
|
1079
|
+
"aes-128-ctr": /* @__PURE__ */ Object.freeze({
|
|
1080
|
+
keyBits: 128,
|
|
1081
|
+
ivBits: 128,
|
|
1082
|
+
name: "AES-CTR"
|
|
1083
|
+
}),
|
|
1084
|
+
"aes-256-cbc": /* @__PURE__ */ Object.freeze({
|
|
1085
|
+
keyBits: 256,
|
|
1086
|
+
ivBits: 128,
|
|
1087
|
+
name: "AES-CBC"
|
|
1088
|
+
}),
|
|
1089
|
+
sha256: /* @__PURE__ */ Object.freeze({
|
|
1090
|
+
keyBits: 256,
|
|
1091
|
+
ivBits: 128,
|
|
1092
|
+
name: "SHA-256"
|
|
1093
|
+
})
|
|
1094
|
+
});
|
|
1095
|
+
const macPrefix = "Fe26.2";
|
|
1096
|
+
async function seal(object, password, opts) {
|
|
1097
|
+
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
|
|
1098
|
+
if (!password) throw new Error("Empty password");
|
|
1099
|
+
const { id = "", encryption, integrity } = normalizePassword(password);
|
|
1100
|
+
if (id && !/^\w+$/.test(id)) throw new Error("Invalid password id");
|
|
1101
|
+
const { encrypted, key } = await encrypt(encryption, opts.encryption, JSON.stringify(object));
|
|
1102
|
+
const encryptedB64 = base64Encode(encrypted);
|
|
1103
|
+
const iv = base64Encode(key.iv);
|
|
1104
|
+
const expiration = opts.ttl ? now + opts.ttl : "";
|
|
1105
|
+
const macBaseString = `${macPrefix}*${id}*${key.salt}*${iv}*${encryptedB64}*${expiration}`;
|
|
1106
|
+
const mac = await hmacWithPassword(integrity, opts.integrity, macBaseString);
|
|
1107
|
+
return `${macBaseString}*${mac.salt}*${mac.digest}`;
|
|
1108
|
+
}
|
|
1109
|
+
async function unseal(sealed, password, opts) {
|
|
1110
|
+
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
|
|
1111
|
+
if (!password) throw new Error("Empty password");
|
|
1112
|
+
const parts = sealed.split("*");
|
|
1113
|
+
if (parts.length !== 8) throw new Error("Incorrect number of sealed components");
|
|
1114
|
+
const [prefix, passwordId, encryptionSalt, encryptionIv, encryptedB64, expiration, hmacSalt, hmac] = parts;
|
|
1115
|
+
const macBaseString = `${prefix}*${passwordId}*${encryptionSalt}*${encryptionIv}*${encryptedB64}*${expiration}`;
|
|
1116
|
+
if ("Fe26.2" !== prefix) throw new Error("Wrong mac prefix");
|
|
1117
|
+
if (expiration) {
|
|
1118
|
+
if (!/^\d+$/.test(expiration)) throw new Error("Invalid expiration");
|
|
1119
|
+
if (Number.parseInt(expiration, 10) <= now - opts.timestampSkewSec * 1e3) throw new Error("Expired seal");
|
|
1120
|
+
}
|
|
1121
|
+
let pass = "";
|
|
1122
|
+
const _passwordId = passwordId || "default";
|
|
1123
|
+
if (typeof password === "string" || password instanceof Uint8Array) pass = password;
|
|
1124
|
+
else if (_passwordId in password) pass = password[_passwordId];
|
|
1125
|
+
else throw new Error(`Cannot find password: ${_passwordId}`);
|
|
1126
|
+
pass = normalizePassword(pass);
|
|
1127
|
+
if (!fixedTimeComparison((await hmacWithPassword(pass.integrity, {
|
|
1128
|
+
...opts.integrity,
|
|
1129
|
+
salt: hmacSalt
|
|
1130
|
+
}, macBaseString)).digest, hmac)) throw new Error("Bad hmac value");
|
|
1131
|
+
const encrypted = base64Decode(encryptedB64);
|
|
1132
|
+
const decryptOptions = {
|
|
1133
|
+
...opts.encryption,
|
|
1134
|
+
salt: encryptionSalt,
|
|
1135
|
+
iv: base64Decode(encryptionIv)
|
|
1136
|
+
};
|
|
1137
|
+
const decrypted = await decrypt(pass.encryption, decryptOptions, encrypted);
|
|
1138
|
+
return decrypted ? JSON.parse(decrypted) : null;
|
|
1139
|
+
}
|
|
1140
|
+
async function hmacWithPassword(password, options, data) {
|
|
1141
|
+
const key = await generateKey(password, {
|
|
1142
|
+
...options,
|
|
1143
|
+
hmac: true
|
|
1144
|
+
});
|
|
1145
|
+
const textBuffer = textEncoder.encode(data);
|
|
1146
|
+
const signed = await crypto.subtle.sign({ name: "HMAC" }, key.key, textBuffer);
|
|
1147
|
+
return {
|
|
1148
|
+
digest: base64Encode(new Uint8Array(signed)),
|
|
1149
|
+
salt: key.salt
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
async function generateKey(password, options) {
|
|
1153
|
+
if (!password?.length) throw new Error("Empty password");
|
|
1154
|
+
if (options == null || typeof options !== "object") throw new Error("Bad options");
|
|
1155
|
+
if (!(options.algorithm in algorithms)) throw new Error(`Unknown algorithm: ${options.algorithm}`);
|
|
1156
|
+
const algorithm = algorithms[options.algorithm];
|
|
1157
|
+
let resultKey;
|
|
1158
|
+
let resultSalt;
|
|
1159
|
+
let resultIV;
|
|
1160
|
+
const hmac = options.hmac ?? false;
|
|
1161
|
+
const id = hmac ? {
|
|
1162
|
+
name: "HMAC",
|
|
1163
|
+
hash: algorithm.name
|
|
1164
|
+
} : { name: algorithm.name };
|
|
1165
|
+
const usage = hmac ? ["sign", "verify"] : ["encrypt", "decrypt"];
|
|
1166
|
+
if (typeof password === "string") {
|
|
1167
|
+
if (password.length < options.minPasswordlength) throw new Error(`Password string too short (min ${options.minPasswordlength} characters required)`);
|
|
1168
|
+
let { salt = "" } = options;
|
|
1169
|
+
if (!salt) {
|
|
1170
|
+
const { saltBits = 0 } = options;
|
|
1171
|
+
if (!saltBits) throw new Error("Missing salt and saltBits options");
|
|
1172
|
+
const randomSalt = randomBits(saltBits);
|
|
1173
|
+
salt = [...new Uint8Array(randomSalt)].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
1174
|
+
}
|
|
1175
|
+
const derivedKey = await pbkdf2(password, salt, options.iterations, algorithm.keyBits / 8, "SHA-1");
|
|
1176
|
+
resultKey = await crypto.subtle.importKey("raw", derivedKey, id, false, usage);
|
|
1177
|
+
resultSalt = salt;
|
|
1178
|
+
} else {
|
|
1179
|
+
if (password.length < algorithm.keyBits / 8) throw new Error("Key buffer (password) too small");
|
|
1180
|
+
resultKey = await crypto.subtle.importKey("raw", password, id, false, usage);
|
|
1181
|
+
resultSalt = "";
|
|
1182
|
+
}
|
|
1183
|
+
if (options.iv) resultIV = options.iv;
|
|
1184
|
+
else if ("ivBits" in algorithm) resultIV = randomBits(algorithm.ivBits);
|
|
1185
|
+
else throw new Error("Missing IV");
|
|
1186
|
+
return {
|
|
1187
|
+
key: resultKey,
|
|
1188
|
+
salt: resultSalt,
|
|
1189
|
+
iv: resultIV
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
async function pbkdf2(password, salt, iterations, keyLength, hash) {
|
|
1193
|
+
const passwordBuffer = textEncoder.encode(password);
|
|
1194
|
+
const importedKey = await crypto.subtle.importKey("raw", passwordBuffer, { name: "PBKDF2" }, false, ["deriveBits"]);
|
|
1195
|
+
const params = {
|
|
1196
|
+
name: "PBKDF2",
|
|
1197
|
+
hash,
|
|
1198
|
+
salt: textEncoder.encode(salt),
|
|
1199
|
+
iterations
|
|
1200
|
+
};
|
|
1201
|
+
return await crypto.subtle.deriveBits(params, importedKey, keyLength * 8);
|
|
1202
|
+
}
|
|
1203
|
+
async function encrypt(password, options, data) {
|
|
1204
|
+
const key = await generateKey(password, options);
|
|
1205
|
+
const encrypted = await crypto.subtle.encrypt(...getEncryptParams(options.algorithm, key, data));
|
|
1206
|
+
return {
|
|
1207
|
+
encrypted: new Uint8Array(encrypted),
|
|
1208
|
+
key
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
async function decrypt(password, options, data) {
|
|
1212
|
+
const key = await generateKey(password, options);
|
|
1213
|
+
const decrypted = await crypto.subtle.decrypt(...getEncryptParams(options.algorithm, key, data));
|
|
1214
|
+
return textDecoder.decode(decrypted);
|
|
1215
|
+
}
|
|
1216
|
+
function getEncryptParams(algorithm, key, data) {
|
|
1217
|
+
return [
|
|
1218
|
+
algorithm === "aes-128-ctr" ? {
|
|
1219
|
+
name: "AES-CTR",
|
|
1220
|
+
counter: key.iv,
|
|
1221
|
+
length: 128
|
|
1222
|
+
} : {
|
|
1223
|
+
name: "AES-CBC",
|
|
1224
|
+
iv: key.iv
|
|
1225
|
+
},
|
|
1226
|
+
key.key,
|
|
1227
|
+
typeof data === "string" ? textEncoder.encode(data) : data
|
|
1228
|
+
];
|
|
1229
|
+
}
|
|
1230
|
+
function fixedTimeComparison(a, b) {
|
|
1231
|
+
let mismatch = a.length === b.length ? 0 : 1;
|
|
1232
|
+
if (mismatch) b = a;
|
|
1233
|
+
for (let i = 0; i < a.length; i += 1) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1234
|
+
return mismatch === 0;
|
|
1235
|
+
}
|
|
1236
|
+
function normalizePassword(password) {
|
|
1237
|
+
if (typeof password === "string" || password instanceof Uint8Array) return {
|
|
1238
|
+
encryption: password,
|
|
1239
|
+
integrity: password
|
|
1240
|
+
};
|
|
1241
|
+
if ("secret" in password) return {
|
|
1242
|
+
id: password.id,
|
|
1243
|
+
encryption: password.secret,
|
|
1244
|
+
integrity: password.secret
|
|
1245
|
+
};
|
|
1246
|
+
return {
|
|
1247
|
+
id: password.id,
|
|
1248
|
+
encryption: password.encryption,
|
|
1249
|
+
integrity: password.integrity
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function randomBits(bits) {
|
|
1253
|
+
if (bits < 1) throw new Error("Invalid random bits count");
|
|
1254
|
+
return randomBytes(Math.ceil(bits / 8));
|
|
1255
|
+
}
|
|
1256
|
+
function randomBytes(size) {
|
|
1257
|
+
const bytes = new Uint8Array(size);
|
|
1258
|
+
crypto.getRandomValues(bytes);
|
|
1259
|
+
return bytes;
|
|
1260
|
+
}
|
|
1261
|
+
const kGetSession = /* @__PURE__ */ Symbol.for("h3.internal.session.promise");
|
|
1262
|
+
const kSessionNew = /* @__PURE__ */ Symbol.for("h3.internal.session.new");
|
|
1263
|
+
const kLegacySeal = /* @__PURE__ */ Symbol.for("h3.internal.session.legacy-seal");
|
|
1264
|
+
const DEFAULT_SESSION_COOKIE = {
|
|
1265
|
+
path: "/",
|
|
1266
|
+
secure: true,
|
|
1267
|
+
httpOnly: true,
|
|
1268
|
+
sameSite: "lax"
|
|
1269
|
+
};
|
|
1270
|
+
async function useSession(event, config) {
|
|
1271
|
+
const sessionName = config.name || "h3";
|
|
1272
|
+
if ((await getSession(event, config))[kSessionNew]) await updateSession(event, config);
|
|
1273
|
+
const sessionManager = {
|
|
1274
|
+
get id() {
|
|
1275
|
+
return getEventContext(event)?.sessions?.[sessionName]?.id;
|
|
1276
|
+
},
|
|
1277
|
+
get data() {
|
|
1278
|
+
return getEventContext(event).sessions?.[sessionName]?.data || new EmptyObject();
|
|
1279
|
+
},
|
|
1280
|
+
update: async (update) => {
|
|
1281
|
+
await updateSession(event, config, update);
|
|
1282
|
+
return sessionManager;
|
|
1283
|
+
},
|
|
1284
|
+
clear: () => {
|
|
1285
|
+
clearSession(event, config);
|
|
1286
|
+
return Promise.resolve(sessionManager);
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
return sessionManager;
|
|
1290
|
+
}
|
|
1291
|
+
async function getSession(event, config) {
|
|
1292
|
+
const sessionName = config.name || "h3";
|
|
1293
|
+
const context = getEventContext(event);
|
|
1294
|
+
if (!context.sessions) context.sessions = new EmptyObject();
|
|
1295
|
+
const existingSession = context.sessions[sessionName];
|
|
1296
|
+
if (existingSession) return existingSession[kGetSession] || existingSession;
|
|
1297
|
+
const session = {
|
|
1298
|
+
id: "",
|
|
1299
|
+
createdAt: 0,
|
|
1300
|
+
data: new EmptyObject()
|
|
1301
|
+
};
|
|
1302
|
+
context.sessions[sessionName] = session;
|
|
1303
|
+
let sealedSession;
|
|
1304
|
+
if (config.sessionHeader !== false) {
|
|
1305
|
+
const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
|
|
1306
|
+
const headerValue = event.req.headers.get(headerName);
|
|
1307
|
+
if (typeof headerValue === "string") sealedSession = headerValue;
|
|
1308
|
+
}
|
|
1309
|
+
let sessionFromCookie = false;
|
|
1310
|
+
if (!sealedSession) {
|
|
1311
|
+
sealedSession = getChunkedCookie(event, sessionName);
|
|
1312
|
+
sessionFromCookie = true;
|
|
1313
|
+
}
|
|
1314
|
+
if (sealedSession) {
|
|
1315
|
+
const promise = unsealSession(event, config, sealedSession).catch(() => {}).then(async (unsealed) => {
|
|
1316
|
+
const legacySeal = unsealed && unsealed[kLegacySeal];
|
|
1317
|
+
if (legacySeal) delete unsealed[kLegacySeal];
|
|
1318
|
+
if (unsealed?.data) unsealed.data = Object.assign(new EmptyObject(), unsealed.data);
|
|
1319
|
+
Object.assign(session, unsealed);
|
|
1320
|
+
delete context.sessions[sessionName][kGetSession];
|
|
1321
|
+
if (session.id && sessionFromCookie && (legacySeal || shouldSlide(session, config))) await updateSession(event, config);
|
|
1322
|
+
return session;
|
|
1323
|
+
});
|
|
1324
|
+
context.sessions[sessionName][kGetSession] = promise;
|
|
1325
|
+
await promise;
|
|
1326
|
+
}
|
|
1327
|
+
if (!session.id) {
|
|
1328
|
+
session.id = config.generateId?.() ?? (config.crypto || crypto).randomUUID();
|
|
1329
|
+
session.createdAt = Date.now();
|
|
1330
|
+
session[kSessionNew] = true;
|
|
1331
|
+
}
|
|
1332
|
+
return session;
|
|
1333
|
+
}
|
|
1334
|
+
async function updateSession(event, config, update) {
|
|
1335
|
+
const sessionName = config.name || "h3";
|
|
1336
|
+
const session = getEventContext(event).sessions?.[sessionName] || await getSession(event, config);
|
|
1337
|
+
if (typeof update === "function") update = update(session.data);
|
|
1338
|
+
if (update) Object.assign(session.data, update);
|
|
1339
|
+
delete session[kSessionNew];
|
|
1340
|
+
if (config.cookie !== false && event.res) {
|
|
1341
|
+
setChunkedCookie(event, sessionName, await sealSession(event, config), {
|
|
1342
|
+
...DEFAULT_SESSION_COOKIE,
|
|
1343
|
+
expires: sessionExpires(session, config),
|
|
1344
|
+
...config.cookie
|
|
1345
|
+
});
|
|
1346
|
+
stageSessionErrCookies(event, sessionName);
|
|
1347
|
+
}
|
|
1348
|
+
return session;
|
|
1349
|
+
}
|
|
1350
|
+
async function sealSession(event, config) {
|
|
1351
|
+
const sessionName = config.name || "h3";
|
|
1352
|
+
const session = getEventContext(event).sessions?.[sessionName] || await getSession(event, config);
|
|
1353
|
+
if (config.idleTimeout) session.lastSeenAt = Date.now();
|
|
1354
|
+
return await seal(session, config.password, {
|
|
1355
|
+
...defaults,
|
|
1356
|
+
ttl: (config.maxAge || config.idleTimeout || 0) * 1e3,
|
|
1357
|
+
...config.seal
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
async function unsealSession(_event, config, sealed) {
|
|
1361
|
+
const sealOptions = {
|
|
1362
|
+
...defaults,
|
|
1363
|
+
ttl: (config.maxAge || config.idleTimeout || 0) * 1e3,
|
|
1364
|
+
...config.seal
|
|
1365
|
+
};
|
|
1366
|
+
let unsealed;
|
|
1367
|
+
try {
|
|
1368
|
+
unsealed = await unseal(sealed, config.password, sealOptions);
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
if (config.legacySealFallback === false || sealOptions.integrity.iterations === 1 || !(error instanceof Error) || error.message !== "Bad hmac value") throw error;
|
|
1371
|
+
unsealed = await unseal(sealed, config.password, {
|
|
1372
|
+
...sealOptions,
|
|
1373
|
+
encryption: {
|
|
1374
|
+
...sealOptions.encryption,
|
|
1375
|
+
iterations: 1
|
|
1376
|
+
},
|
|
1377
|
+
integrity: {
|
|
1378
|
+
...sealOptions.integrity,
|
|
1379
|
+
iterations: 1
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
if (unsealed) unsealed[kLegacySeal] = true;
|
|
1383
|
+
}
|
|
1384
|
+
if (config.maxAge) {
|
|
1385
|
+
if (Date.now() - (unsealed.createdAt || Number.NEGATIVE_INFINITY) > config.maxAge * 1e3) throw new Error("Session expired!");
|
|
1386
|
+
}
|
|
1387
|
+
if (config.idleTimeout) {
|
|
1388
|
+
if (Date.now() - (unsealed.lastSeenAt || unsealed.createdAt || Number.NEGATIVE_INFINITY) > config.idleTimeout * 1e3) throw new Error("Session expired!");
|
|
1389
|
+
}
|
|
1390
|
+
return unsealed;
|
|
1391
|
+
}
|
|
1392
|
+
function clearSession(event, config) {
|
|
1393
|
+
const context = getEventContext(event);
|
|
1394
|
+
const sessionName = config.name || "h3";
|
|
1395
|
+
if (context.sessions?.[sessionName]) delete context.sessions[sessionName];
|
|
1396
|
+
if (event.res && config.cookie !== false) {
|
|
1397
|
+
deleteChunkedCookie(event, sessionName, {
|
|
1398
|
+
...DEFAULT_SESSION_COOKIE,
|
|
1399
|
+
...config.cookie
|
|
1400
|
+
});
|
|
1401
|
+
stageSessionErrCookies(event, sessionName);
|
|
1402
|
+
}
|
|
1403
|
+
return Promise.resolve();
|
|
1404
|
+
}
|
|
1405
|
+
const SLIDE_THRESHOLD = .5;
|
|
1406
|
+
function shouldSlide(session, config) {
|
|
1407
|
+
if (!config.idleTimeout) return false;
|
|
1408
|
+
const lastSeenAt = session.lastSeenAt || session.createdAt || 0;
|
|
1409
|
+
return Date.now() - lastSeenAt > config.idleTimeout * 1e3 * SLIDE_THRESHOLD;
|
|
1410
|
+
}
|
|
1411
|
+
function stageSessionErrCookies(event, sessionName) {
|
|
1412
|
+
const isSessionCookie = (cookie) => cookie.startsWith(`${sessionName}=`) || cookie.startsWith(`${sessionName}.`);
|
|
1413
|
+
const errHeaders = event.res.errHeaders;
|
|
1414
|
+
const staged = [...errHeaders.getSetCookie().filter((cookie) => !isSessionCookie(cookie)), ...event.res.headers.getSetCookie().filter((cookie) => isSessionCookie(cookie))];
|
|
1415
|
+
errHeaders.delete("set-cookie");
|
|
1416
|
+
for (const cookie of staged) errHeaders.append("set-cookie", cookie);
|
|
1417
|
+
}
|
|
1418
|
+
function sessionExpires(session, config) {
|
|
1419
|
+
const times = [];
|
|
1420
|
+
if (config.maxAge) times.push(session.createdAt + config.maxAge * 1e3);
|
|
1421
|
+
if (config.idleTimeout) times.push((session.lastSeenAt || session.createdAt) + config.idleTimeout * 1e3);
|
|
1422
|
+
return times.length > 0 ? new Date(Math.min(...times)) : void 0;
|
|
1423
|
+
}
|
|
1424
|
+
const _textEncoder = /* @__PURE__ */ new TextEncoder();
|
|
1425
|
+
function timingSafeEqual(a, b) {
|
|
1426
|
+
const aBuf = _textEncoder.encode(a);
|
|
1427
|
+
const bBuf = _textEncoder.encode(b);
|
|
1428
|
+
const aLen = aBuf.length;
|
|
1429
|
+
const bLen = bBuf.length;
|
|
1430
|
+
const len = Math.max(aLen, bLen);
|
|
1431
|
+
let result = aLen === bLen ? 0 : 1;
|
|
1432
|
+
for (let i = 0; i < len; i++) result |= (aBuf[i % aLen] ?? 0) ^ (bBuf[i % bLen] ?? 0);
|
|
1433
|
+
return result === 0;
|
|
1434
|
+
}
|
|
1435
|
+
function randomJitter() {
|
|
1436
|
+
const randomBuffer = /* @__PURE__ */ new Uint32Array(1);
|
|
1437
|
+
crypto.getRandomValues(randomBuffer);
|
|
1438
|
+
const jitter = randomBuffer[0] % 100;
|
|
1439
|
+
return new Promise((resolve) => setTimeout(resolve, jitter));
|
|
1440
|
+
}
|
|
1441
|
+
async function requireBasicAuth(event, opts) {
|
|
1442
|
+
if (!opts.validate && !opts.password) throw new HTTPError({
|
|
1443
|
+
message: "Either 'password' or 'validate' option must be provided",
|
|
1444
|
+
status: 500
|
|
1445
|
+
});
|
|
1446
|
+
const realm = opts?.realm ?? "auth";
|
|
1447
|
+
const authHeader = event.req.headers.get("authorization");
|
|
1448
|
+
if (!authHeader) throw await authFailed(event, realm);
|
|
1449
|
+
const b64auth = /^basic +(.+)$/i.exec(authHeader)?.[1];
|
|
1450
|
+
if (!b64auth) throw await authFailed(event, realm);
|
|
1451
|
+
let authDecoded;
|
|
1452
|
+
try {
|
|
1453
|
+
authDecoded = atob(b64auth);
|
|
1454
|
+
} catch {
|
|
1455
|
+
throw await authFailed(event, realm);
|
|
1456
|
+
}
|
|
1457
|
+
try {
|
|
1458
|
+
authDecoded = new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(authDecoded, (c) => c.charCodeAt(0)));
|
|
1459
|
+
} catch {}
|
|
1460
|
+
const colonIndex = authDecoded.indexOf(":");
|
|
1461
|
+
if (colonIndex === -1) throw await authFailed(event, realm);
|
|
1462
|
+
const username = authDecoded.slice(0, colonIndex);
|
|
1463
|
+
const password = authDecoded.slice(colonIndex + 1);
|
|
1464
|
+
if (!opts.validate && (!username || !password)) throw await authFailed(event, realm);
|
|
1465
|
+
const usernameOk = !opts.username || timingSafeEqual(username, opts.username);
|
|
1466
|
+
const passwordOk = !opts.password || timingSafeEqual(password, opts.password);
|
|
1467
|
+
const validateOk = !opts.validate || await opts.validate(username, password);
|
|
1468
|
+
if (!usernameOk || !passwordOk || !validateOk) throw await authFailed(event, realm);
|
|
1469
|
+
const context = getEventContext(event);
|
|
1470
|
+
context.basicAuth = {
|
|
1471
|
+
username,
|
|
1472
|
+
password,
|
|
1473
|
+
realm
|
|
1474
|
+
};
|
|
1475
|
+
return true;
|
|
1476
|
+
}
|
|
1477
|
+
function basicAuth(opts) {
|
|
1478
|
+
return async (event, next) => {
|
|
1479
|
+
await requireBasicAuth(event, opts);
|
|
1480
|
+
return next();
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
async function authFailed(event, realm) {
|
|
1484
|
+
await randomJitter();
|
|
1485
|
+
return new HTTPError({
|
|
1486
|
+
status: 401,
|
|
1487
|
+
statusText: "Authentication required",
|
|
1488
|
+
headers: { "www-authenticate": `Basic realm="${quoteRealm(realm)}", charset="UTF-8"` }
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
function quoteRealm(realm) {
|
|
1492
|
+
return realm.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "").replace(/["\\]/g, "\\$&");
|
|
1493
|
+
}
|
|
1494
|
+
async function getRequestFingerprint(event, opts = {}) {
|
|
1495
|
+
const fingerprint = [];
|
|
1496
|
+
let hasValue = false;
|
|
1497
|
+
const addComponent = (value) => {
|
|
1498
|
+
if (value) hasValue = true;
|
|
1499
|
+
fingerprint.push(value ? escapeComponent(value) : "");
|
|
1500
|
+
};
|
|
1501
|
+
if (opts.ip !== false) addComponent(getRequestIP(event, { xForwardedFor: opts.xForwardedFor }));
|
|
1502
|
+
if (opts.method === true) addComponent(event.req.method);
|
|
1503
|
+
if (opts.url === true) addComponent(event.req.url);
|
|
1504
|
+
if (opts.userAgent === true) addComponent(event.req.headers.get("user-agent"));
|
|
1505
|
+
if (!hasValue) return null;
|
|
1506
|
+
const fingerprintString = fingerprint.join("|");
|
|
1507
|
+
if (opts.hash === false) return fingerprintString;
|
|
1508
|
+
const buffer = await crypto.subtle.digest(opts.hash || "SHA-256", new TextEncoder().encode(fingerprintString));
|
|
1509
|
+
return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1510
|
+
}
|
|
1511
|
+
function escapeComponent(value) {
|
|
1512
|
+
return value.replaceAll("%", "%25").replaceAll("|", "%7C");
|
|
1513
|
+
}
|
|
1514
|
+
function defineWebSocket(hooks) {
|
|
1515
|
+
return hooks;
|
|
1516
|
+
}
|
|
1517
|
+
function defineWebSocketHandler(hooks, http) {
|
|
1518
|
+
return defineHandler(function _webSocketHandler(event) {
|
|
1519
|
+
if (http && !isWebSocketUpgrade(event)) return http(event);
|
|
1520
|
+
const crossws = typeof hooks === "function" ? hooks(event) : hooks;
|
|
1521
|
+
if (crossws instanceof Promise) return crossws.then((resolved) => toUpgradeResponse(event, resolved));
|
|
1522
|
+
return toUpgradeResponse(event, crossws);
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
function isWebSocketUpgrade(event) {
|
|
1526
|
+
return event.req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
1527
|
+
}
|
|
1528
|
+
const kWebSocketHooks = /* @__PURE__ */ Symbol.for("crossws.hooks");
|
|
1529
|
+
function toUpgradeResponse(event, crossws) {
|
|
1530
|
+
try {
|
|
1531
|
+
const req = event.req;
|
|
1532
|
+
if (req.context) req.context[kWebSocketHooks] = crossws;
|
|
1533
|
+
req[kWebSocketHooks] = crossws;
|
|
1534
|
+
} catch {}
|
|
1535
|
+
return Object.assign(new Response("WebSocket upgrade is required.", { status: 426 }), { crossws });
|
|
1536
|
+
}
|
|
1537
|
+
const PARSE_ERROR = -32700;
|
|
1538
|
+
const INVALID_REQUEST = -32600;
|
|
1539
|
+
const METHOD_NOT_FOUND = -32601;
|
|
1540
|
+
const INVALID_PARAMS = -32602;
|
|
1541
|
+
const DEFAULT_MAX_BATCH_SIZE = 50;
|
|
1542
|
+
function defineJsonRpcHandler(opts = {}) {
|
|
1543
|
+
const methodMap = createMethodMap(opts.methods);
|
|
1544
|
+
const maxBatchSize = opts.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
1545
|
+
const handler = async (event) => {
|
|
1546
|
+
if (event.req.method !== "POST") throw new HTTPError({ status: 405 });
|
|
1547
|
+
if (opts.validateContentType !== false && !isJsonContentType(event.req.headers.get("content-type"))) throw new HTTPError({
|
|
1548
|
+
status: 415,
|
|
1549
|
+
message: "Unsupported Media Type"
|
|
1550
|
+
});
|
|
1551
|
+
assertAllowedOrigin(event, opts.allowedOrigins);
|
|
1552
|
+
let body;
|
|
1553
|
+
try {
|
|
1554
|
+
body = await event.req.json();
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
if (HTTPError.isError(error)) throw error;
|
|
1557
|
+
return createJsonRpcError(null, PARSE_ERROR, "Parse error");
|
|
1558
|
+
}
|
|
1559
|
+
const result = await processJsonRpcBody(body, methodMap, event, maxBatchSize);
|
|
1560
|
+
return result === void 0 ? new HTTPResponse("", { status: 202 }) : result;
|
|
1561
|
+
};
|
|
1562
|
+
return defineHandler({
|
|
1563
|
+
...opts,
|
|
1564
|
+
handler
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
function isJsonContentType(value) {
|
|
1568
|
+
if (!value) return false;
|
|
1569
|
+
const mediaType = value.split(";")[0].trim().toLowerCase();
|
|
1570
|
+
return mediaType === "application/json" || mediaType === "application/json-rpc" || mediaType.endsWith("+json");
|
|
1571
|
+
}
|
|
1572
|
+
function assertAllowedOrigin(event, allowedOrigins) {
|
|
1573
|
+
const origin = event.req.headers.get("origin");
|
|
1574
|
+
if (!origin || allowedOrigins === "*") return;
|
|
1575
|
+
if (!(allowedOrigins ? isCorsOriginAllowed(origin, { origin: typeof allowedOrigins === "string" ? [allowedOrigins] : allowedOrigins }) : origin === event.url.origin)) throw new HTTPError({
|
|
1576
|
+
status: 403,
|
|
1577
|
+
message: "Origin not allowed"
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
function defineJsonRpcWebSocketHandler(opts) {
|
|
1581
|
+
const methodMap = createMethodMap(opts.methods);
|
|
1582
|
+
const maxBatchSize = opts.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
1583
|
+
return defineWebSocketHandler({
|
|
1584
|
+
...opts.hooks,
|
|
1585
|
+
async message(peer, message) {
|
|
1586
|
+
let body;
|
|
1587
|
+
try {
|
|
1588
|
+
body = message.json();
|
|
1589
|
+
} catch {
|
|
1590
|
+
peer.send(JSON.stringify(createJsonRpcError(null, PARSE_ERROR, "Parse error")));
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const result = await processJsonRpcBody(body, methodMap, peer, maxBatchSize);
|
|
1594
|
+
if (result !== void 0) peer.send(JSON.stringify(result));
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
function createMethodMap(methods) {
|
|
1599
|
+
const methodMap = Object.create(null);
|
|
1600
|
+
for (const key of Object.keys(methods)) methodMap[key] = methods[key];
|
|
1601
|
+
return methodMap;
|
|
1602
|
+
}
|
|
1603
|
+
async function processJsonRpcBody(body, methodMap, context, maxBatchSize) {
|
|
1604
|
+
if (!body || typeof body !== "object") return createJsonRpcError(null, INVALID_REQUEST, "Invalid Request");
|
|
1605
|
+
const requests = Array.isArray(body) ? body : [body];
|
|
1606
|
+
if (requests.length === 0) return createJsonRpcError(null, INVALID_REQUEST, "Invalid Request");
|
|
1607
|
+
if (requests.length > maxBatchSize) return createJsonRpcError(null, INVALID_REQUEST, `Invalid Request: batch size exceeds maximum of ${maxBatchSize}`);
|
|
1608
|
+
const finalResponses = (await Promise.all(requests.map((raw) => processJsonRpcMethod(raw, methodMap, context)))).filter((r) => r !== void 0);
|
|
1609
|
+
if (finalResponses.length === 0) return;
|
|
1610
|
+
return Array.isArray(body) ? finalResponses : finalResponses[0];
|
|
1611
|
+
}
|
|
1612
|
+
async function processJsonRpcMethod(raw, methodMap, context) {
|
|
1613
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return createJsonRpcError(null, INVALID_REQUEST, "Invalid Request");
|
|
1614
|
+
const req = raw;
|
|
1615
|
+
if (req.jsonrpc !== "2.0" || typeof req.method !== "string" || "id" in req && !isValidId(req.id)) {
|
|
1616
|
+
const id = "id" in req && isValidId(req.id) ? req.id : null;
|
|
1617
|
+
return createJsonRpcError(id, INVALID_REQUEST, "Invalid Request");
|
|
1618
|
+
}
|
|
1619
|
+
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");
|
|
1620
|
+
if (req.method.startsWith("rpc.")) return isNotification(req) ? void 0 : createJsonRpcError(req.id, METHOD_NOT_FOUND, "Method not found");
|
|
1621
|
+
const method = req.method;
|
|
1622
|
+
const params = req.params;
|
|
1623
|
+
const notification = isNotification(req);
|
|
1624
|
+
const id = notification ? void 0 : req.id;
|
|
1625
|
+
const methodHandler = methodMap[method];
|
|
1626
|
+
if (!methodHandler) return notification ? void 0 : createJsonRpcError(id, METHOD_NOT_FOUND, "Method not found");
|
|
1627
|
+
try {
|
|
1628
|
+
const rpcReq = {
|
|
1629
|
+
jsonrpc: "2.0",
|
|
1630
|
+
method,
|
|
1631
|
+
params
|
|
1632
|
+
};
|
|
1633
|
+
if (!notification) rpcReq.id = id;
|
|
1634
|
+
const result = await methodHandler(rpcReq, context);
|
|
1635
|
+
return notification ? void 0 : {
|
|
1636
|
+
jsonrpc: "2.0",
|
|
1637
|
+
id,
|
|
1638
|
+
result: result ?? null
|
|
1639
|
+
};
|
|
1640
|
+
} catch (error_) {
|
|
1641
|
+
if (notification) return;
|
|
1642
|
+
const h3Error = HTTPError.isError(error_) && !error_.unhandled ? error_ : {
|
|
1643
|
+
status: HTTPError.isError(error_) ? error_.status : 500,
|
|
1644
|
+
message: "Internal error",
|
|
1645
|
+
data: void 0
|
|
1646
|
+
};
|
|
1647
|
+
const statusCode = h3Error.status;
|
|
1648
|
+
const statusMessage = h3Error.message;
|
|
1649
|
+
const errorCode = mapHttpStatusToJsonRpcError(statusCode);
|
|
1650
|
+
return createJsonRpcError(id, errorCode, statusMessage, h3Error.data);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function mapHttpStatusToJsonRpcError(status) {
|
|
1654
|
+
switch (status) {
|
|
1655
|
+
case 400:
|
|
1656
|
+
case 422: return INVALID_PARAMS;
|
|
1657
|
+
case 401: return -32001;
|
|
1658
|
+
case 403: return -32003;
|
|
1659
|
+
case 404: return -32004;
|
|
1660
|
+
case 408: return -32008;
|
|
1661
|
+
case 409: return -32009;
|
|
1662
|
+
case 429: return -32029;
|
|
1663
|
+
default:
|
|
1664
|
+
if (status >= 300 && status < 500) return -32e3;
|
|
1665
|
+
return -32603;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function isNotification(req) {
|
|
1669
|
+
return !("id" in req);
|
|
1670
|
+
}
|
|
1671
|
+
function isValidId(id) {
|
|
1672
|
+
if (id === null) return true;
|
|
1673
|
+
if (typeof id === "string") return true;
|
|
1674
|
+
return typeof id === "number" && Number.isInteger(id);
|
|
1675
|
+
}
|
|
1676
|
+
const createJsonRpcError = (id, code, message, data) => {
|
|
1677
|
+
const error = {
|
|
1678
|
+
code,
|
|
1679
|
+
message
|
|
1680
|
+
};
|
|
1681
|
+
if (data !== void 0) error.data = data;
|
|
1682
|
+
return {
|
|
1683
|
+
jsonrpc: "2.0",
|
|
1684
|
+
id,
|
|
1685
|
+
error
|
|
1686
|
+
};
|
|
1687
|
+
};
|
|
1688
|
+
const H3Error = HTTPError;
|
|
1689
|
+
function createError(arg1, arg2) {
|
|
1690
|
+
return new HTTPError(arg1, arg2);
|
|
1691
|
+
}
|
|
1692
|
+
function isError(input) {
|
|
1693
|
+
return HTTPError.isError(input);
|
|
1694
|
+
}
|
|
1695
|
+
const getRequestPath = (event) => event.path;
|
|
1696
|
+
function getRequestHeader(event, name) {
|
|
1697
|
+
return event.req.headers.get(name) || void 0;
|
|
1698
|
+
}
|
|
1699
|
+
const getHeader = getRequestHeader;
|
|
1700
|
+
function getRequestHeaders(event) {
|
|
1701
|
+
return Object.fromEntries(event.req.headers.entries());
|
|
1702
|
+
}
|
|
1703
|
+
const getHeaders = getRequestHeaders;
|
|
1704
|
+
function getMethod(event, defaultMethod = "GET") {
|
|
1705
|
+
return (event.req.method || defaultMethod).toUpperCase();
|
|
1706
|
+
}
|
|
1707
|
+
function readRawBody(event, encoding = "utf8") {
|
|
1708
|
+
return encoding ? event.req.text() : event.req.arrayBuffer().then((r) => new Uint8Array(r));
|
|
1709
|
+
}
|
|
1710
|
+
async function readFormDataBody(event) {
|
|
1711
|
+
return event.req.formData();
|
|
1712
|
+
}
|
|
1713
|
+
const readFormData = readFormDataBody;
|
|
1714
|
+
async function readMultipartFormData(event) {
|
|
1715
|
+
const formData = await event.req.formData();
|
|
1716
|
+
return Promise.all([...formData.entries()].map(async ([key, value]) => {
|
|
1717
|
+
return typeof value === "object" ? {
|
|
1718
|
+
name: key,
|
|
1719
|
+
type: value.type,
|
|
1720
|
+
filename: value.name,
|
|
1721
|
+
data: await value.bytes()
|
|
1722
|
+
} : {
|
|
1723
|
+
name: key,
|
|
1724
|
+
data: new TextEncoder().encode(value)
|
|
1725
|
+
};
|
|
1726
|
+
}));
|
|
1727
|
+
}
|
|
1728
|
+
function getBodyStream(event) {
|
|
1729
|
+
return event.req.body || void 0;
|
|
1730
|
+
}
|
|
1731
|
+
const getRequestWebStream = getBodyStream;
|
|
1732
|
+
function sendStream(_event, value) {
|
|
1733
|
+
return value;
|
|
1734
|
+
}
|
|
1735
|
+
const sendNoContent = (_, code) => noContent(code);
|
|
1736
|
+
const sendRedirect = (_, loc, code) => redirect(loc, code);
|
|
1737
|
+
const sendWebResponse = (response) => response;
|
|
1738
|
+
const sendProxy = proxy;
|
|
1739
|
+
function createEventStream(event, opts) {
|
|
1740
|
+
return new EventStream(event, opts);
|
|
1741
|
+
}
|
|
1742
|
+
const sendIterable = (_event, val, options) => {
|
|
1743
|
+
return iterable(val, options);
|
|
1744
|
+
};
|
|
1745
|
+
function getResponseStatusText(event) {
|
|
1746
|
+
return event.res.statusText || "";
|
|
1747
|
+
}
|
|
1748
|
+
function appendResponseHeader(event, name, value) {
|
|
1749
|
+
if (Array.isArray(value)) for (const valueItem of value) event.res.headers.append(name, valueItem);
|
|
1750
|
+
else event.res.headers.append(name, value);
|
|
1751
|
+
}
|
|
1752
|
+
const appendHeader = appendResponseHeader;
|
|
1753
|
+
function setResponseHeader(event, name, value) {
|
|
1754
|
+
if (Array.isArray(value)) {
|
|
1755
|
+
event.res.headers.delete(name);
|
|
1756
|
+
for (const valueItem of value) event.res.headers.append(name, valueItem);
|
|
1757
|
+
} else event.res.headers.set(name, value);
|
|
1758
|
+
}
|
|
1759
|
+
const setHeader = setResponseHeader;
|
|
1760
|
+
function setResponseHeaders(event, headers) {
|
|
1761
|
+
for (const [name, value] of Object.entries(headers)) event.res.headers.set(name, value);
|
|
1762
|
+
}
|
|
1763
|
+
const setHeaders = setResponseHeaders;
|
|
1764
|
+
function getResponseStatus(event) {
|
|
1765
|
+
return event.res.status || 200;
|
|
1766
|
+
}
|
|
1767
|
+
function setResponseStatus(event, code, text) {
|
|
1768
|
+
if (code) event.res.status = sanitizeStatusCode(code, event.res.status);
|
|
1769
|
+
if (text) event.res.statusText = sanitizeStatusMessage(text);
|
|
1770
|
+
}
|
|
1771
|
+
function defaultContentType(event, type) {
|
|
1772
|
+
if (type && event.res.status !== 304 && !event.res.headers.has("content-type")) event.res.headers.set("content-type", type);
|
|
1773
|
+
}
|
|
1774
|
+
function getResponseHeaders(event) {
|
|
1775
|
+
return Object.fromEntries(event.res.headers.entries());
|
|
1776
|
+
}
|
|
1777
|
+
function getResponseHeader(event, name) {
|
|
1778
|
+
return event.res.headers.get(name) || void 0;
|
|
1779
|
+
}
|
|
1780
|
+
function removeResponseHeader(event, name) {
|
|
1781
|
+
return event.res.headers.delete(name);
|
|
1782
|
+
}
|
|
1783
|
+
function appendResponseHeaders(event, headers) {
|
|
1784
|
+
for (const [name, value] of Object.entries(headers)) appendResponseHeader(event, name, value);
|
|
1785
|
+
}
|
|
1786
|
+
const appendHeaders = appendResponseHeaders;
|
|
1787
|
+
function clearResponseHeaders(event, headerNames) {
|
|
1788
|
+
if (headerNames && headerNames.length > 0) for (const name of headerNames) event.res.headers.delete(name);
|
|
1789
|
+
else for (const name of event.res.headers.keys()) event.res.headers.delete(name);
|
|
1790
|
+
}
|
|
1791
|
+
const defineEventHandler = defineHandler;
|
|
1792
|
+
const eventHandler = defineHandler;
|
|
1793
|
+
const lazyEventHandler = defineLazyEventHandler;
|
|
1794
|
+
const defineNodeListener = defineNodeHandler;
|
|
1795
|
+
const fromNodeMiddleware = fromNodeHandler;
|
|
1796
|
+
function toNodeHandler(app) {
|
|
1797
|
+
if (toNodeHandler._isWarned !== true) {
|
|
1798
|
+
console.warn(`[h3] "toNodeHandler" export from h3 is deprecated. Please import "toNodeHandler" from "h3/node".`);
|
|
1799
|
+
toNodeHandler._isWarned = true;
|
|
1800
|
+
}
|
|
1801
|
+
return (toNodeHandler._toNodeHandler ??= () => {
|
|
1802
|
+
return globalThis.process.getBuiltinModule("node:module").createRequire(import.meta.url)("srvx/node").toNodeHandler;
|
|
1803
|
+
})()(app.fetch);
|
|
1804
|
+
}
|
|
1805
|
+
const toNodeListener = toNodeHandler;
|
|
1806
|
+
const createApp = (config) => new H3(config);
|
|
1807
|
+
const createRouter$1 = (config) => new H3(config);
|
|
1808
|
+
const useBase = withBase;
|
|
1809
|
+
export { EventStream, H3Error, appendAcceptQuery, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertBodySize, basicAuth, bodyLimit, clearResponseHeaders, clearSession, createApp, createError, createEventStream, createRouter$1 as createRouter, defaultContentType, defineEventHandler, defineJsonRpcHandler, defineJsonRpcWebSocketHandler, defineNodeHandler, defineNodeListener, defineNodeMiddleware, definePlugin, defineRoute, defineWebSocket, defineWebSocketHandler, deleteChunkedCookie, deleteCookie, eventHandler, freezeApp, fromNodeHandler, fromNodeMiddleware, fromWebHandler, getBodyStream, getChunkedCookie, getCookie, getHeader, getHeaders, getMethod, getRequestFingerprint, getRequestHeader, getRequestHeaders, getRequestPath, getRequestWebStream, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getSession, getValidatedCookies, isError, lazyEventHandler, onError, onRequest, onResponse, parseCookies, readBody, readFormData, readFormDataBody, readMultipartFormData, readRawBody, readValidatedBody, removeResponseHeader, removeRoute$1 as removeRoute, requireBasicAuth, requireContentType, sealSession, sendIterable, sendNoContent, sendProxy, sendRedirect, sendStream, sendWebResponse, serveStatic, setChunkedCookie, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, setServerTiming, toNodeHandler, toNodeListener, toWebHandler, unsealSession, updateSession, useBase, useSession, withBase, withServerTiming };
|