h3
Version:
Minimal H(TTP) framework built for high performance and portability.
1,731 lines • 62.3 kB
JavaScript
import { EmptyObject, HTTPError, HTTPResponse, decodePreservingSeparators, hasProp, kHandled, normalizeRoute, onDispose, sanitizeStatusCode, sanitizeStatusMessage, toResponse, withoutBase, withoutTrailingSlash } from "./response.mjs";
import "./middleware.mjs";
import { H3, defineHandler, defineLazyEventHandler, defineValidatedHandler, getEventContext, getRequestIP, isCacheMatch, toEventHandler, validateData } from "./cache.mjs";
import { base64Decode, base64Encode, isCorsOriginAllowed, iterable, noContent, redirect, textDecoder, textEncoder } from "./cors.mjs";
import { proxy } from "./proxy.mjs";
import { isCanonicalPath } from "./path.mjs";
import { addRoute, createRouter, removeRoute } from "rou3";
import { limitRequestBody } from "srvx/body-limit";
function freezeApp(app) {
app.config = Object.freeze(app.config);
app["~addRoute"] = () => {
throw new Error("Cannot add routes after the server init.");
};
}
function definePlugin(def) {
return ((opts) => (h3) => def(h3, opts));
}
function toWebHandler(app) {
return (request, context) => {
return Promise.resolve(app.request(request, void 0, context || request.context));
};
}
function fromWebHandler(handler) {
return function _webHandler(event) {
return handler(event.req, event.context);
};
}
function fromNodeHandler(handler) {
if (typeof handler !== "function") throw new TypeError(`Invalid handler. It should be a function: ${handler}`);
return function _nodeHandler(event) {
const node = event.runtime?.node;
if (!node?.res) throw new Error("[h3] Executing Node.js middleware is not supported in this server!");
const url = event.url.pathname + event.url.search;
if (node.req.url === url) return callNodeHandler(handler, node.req, node.res);
const originalUrl = node.req.url;
node.req.url = url;
return callNodeHandler(handler, node.req, node.res).finally(() => {
node.req.url = originalUrl;
});
};
}
function defineNodeHandler(handler) {
return handler;
}
function defineNodeMiddleware(handler) {
return handler;
}
function callNodeHandler(handler, req, res) {
const isMiddleware = handler.length > 2;
return new Promise((resolve, reject) => {
res.once("close", () => resolve(kHandled));
res.once("finish", () => resolve(kHandled));
res.once("error", (error) => reject(error));
res.once("pipe", (stream) => {
resolve(new Promise((resolve, reject) => {
const onResClose = () => {
stream.destroy();
resolve(kHandled);
};
const settle = (cb) => {
res.removeListener("close", onResClose);
cb();
};
stream.once("close", () => settle(() => resolve(kHandled)));
stream.once("error", (error) => settle(() => {
console.error("[h3] Stream error in Node.js handler", { cause: error });
reject(kHandled);
}));
if (res.closed || res.destroyed) onResClose();
else res.once("close", onResClose);
}));
});
try {
if (isMiddleware) Promise.resolve(handler(req, res, (error) => error ? reject(new HTTPError({
cause: error,
unhandled: true
})) : resolve(void 0))).catch((error) => reject(new HTTPError({
cause: error,
unhandled: true
})));
else return Promise.resolve(handler(req, res)).then(() => resolve(kHandled)).catch((error) => reject(new HTTPError({
cause: error,
unhandled: true
})));
} catch (error) {
reject(new HTTPError({
cause: error,
unhandled: true
}));
}
});
}
function defineRoute(def) {
const handler = defineValidatedHandler(def);
return (h3) => {
h3.on(def.method, def.route, handler);
};
}
function removeRoute$1(app, method, route) {
const _method = method ? method.toUpperCase() : "";
route = normalizeRoute(route);
const routes = app["~routes"];
const kept = routes.filter((r) => !(r.route === route && (r.method || "") === _method));
if (kept.length === routes.length) {
removeRoute(app["~rou3"], _method, route);
return;
}
app["~routes"] = kept;
const rou3 = app["~rou3"];
if (rou3) {
const rebuilt = createRouter();
for (const r of kept) addRoute(rebuilt, r.method || "", r.route, r);
rou3.root = rebuilt.root;
rou3.static = rebuilt.static;
}
}
function appendAcceptQuery(event, mediaTypes) {
const list = Array.isArray(mediaTypes) ? mediaTypes : [mediaTypes];
if (list.length === 0) return;
const value = list.map(serializeMediaType).join(", ");
event.res.headers.append("accept-query", value);
}
function requireContentType(event, acceptedTypes) {
const header = event.req.headers.get("content-type");
if (!header) throw new HTTPError({
status: 400,
statusText: "Bad Request",
message: "Content-Type header is required"
});
const mediaType = header.split(";")[0].trim().toLowerCase();
const slash = mediaType.indexOf("/");
if (slash <= 0 || slash === mediaType.length - 1) throw new HTTPError({
status: 422,
statusText: "Unprocessable Content",
message: "Malformed Content-Type header"
});
const accepted = Array.isArray(acceptedTypes) ? acceptedTypes : [acceptedTypes];
if (accepted.some((type) => mediaTypeMatches(mediaType, type.split(";")[0].trim().toLowerCase()))) return mediaType;
throw new HTTPError({
status: 415,
statusText: "Unsupported Media Type",
message: `Unsupported Content-Type: ${mediaType}. Expected one of: ${accepted.join(", ")}`
});
}
const SF_TOKEN_RE = /^[A-Za-z*][\w!#$%&'*+.^`|~:/-]*$/;
const SF_KEY_RE = /^[a-z*][a-z0-9_.*-]*$/;
function serializeMediaType(mediaType) {
const parts = splitOutsideQuotes(mediaType, ";");
const base = parts[0].trim();
if (!SF_TOKEN_RE.test(base)) throw new TypeError(`Invalid media type: ${JSON.stringify(mediaType)}`);
let result = base;
for (let i = 1; i < parts.length; i++) {
const param = parts[i].trim();
if (!param) continue;
const eq = param.indexOf("=");
const key = (eq === -1 ? param : param.slice(0, eq)).trim().toLowerCase();
if (!SF_KEY_RE.test(key)) throw new TypeError(`Invalid media type parameter: ${JSON.stringify(param)}`);
result += eq === -1 ? `;${key}` : `;${key}="${escapeQuotes(unquote(param.slice(eq + 1).trim()))}"`;
}
return result;
}
function mediaTypeMatches(mediaType, accepted) {
if (accepted === "*/*" || accepted === "*") return true;
if (accepted === mediaType) return true;
if (accepted.endsWith("/*")) return mediaType.startsWith(accepted.slice(0, -1));
return false;
}
function splitOutsideQuotes(input, sep) {
const parts = [];
let current = "";
let inQuotes = false;
for (let i = 0; i < input.length; i++) {
const ch = input[i];
if (inQuotes) {
current += ch;
if (ch === "\\" && i + 1 < input.length) current += input[++i];
else if (ch === "\"") inQuotes = false;
} else if (ch === "\"") {
inQuotes = true;
current += ch;
} else if (ch === sep) {
parts.push(current);
current = "";
} else current += ch;
}
parts.push(current);
return parts;
}
function escapeQuotes(value) {
return value.replace(/[\\"]/g, "\\$&");
}
function unquote(value) {
if (value.length >= 2 && value[0] === "\"" && value.endsWith("\"")) return value.slice(1, -1).replace(/\\(.)/g, "$1");
return value;
}
function parseURLEncodedBody(body) {
return collectEntries(new URLSearchParams(body).entries());
}
function parseFormData(form) {
return collectEntries(form.entries());
}
function collectEntries(entries) {
const parsed = new EmptyObject();
for (const [key, value] of entries) if (hasProp(parsed, key)) {
if (!Array.isArray(parsed[key])) parsed[key] = [parsed[key]];
parsed[key].push(value);
} else parsed[key] = value;
return parsed;
}
async function readBody(event, options) {
const contentType = event.req.headers.get("content-type") || "";
const type = options?.type;
if (type === "formData") {
let form;
try {
form = await event.req.formData();
} catch (error) {
if (HTTPError.isError(error)) throw error;
throw new HTTPError({
status: 400,
statusText: "Bad Request",
message: "Invalid form data body"
});
}
return parseFormData(form);
}
const text = await event.req.text();
if (type === "text") return text;
if (!text) return;
if (type === "urlencoded" || !type && contentType.startsWith("application/x-www-form-urlencoded")) return parseURLEncodedBody(text);
try {
return JSON.parse(text);
} catch {
throw new HTTPError({
status: 400,
statusText: "Bad Request",
message: "Invalid JSON body"
});
}
}
async function readValidatedBody(event, validate, options) {
const _body = await readBody(event, options);
return validateData(_body, validate, options);
}
function assertBodySize(event, limit) {
const req = event.req;
if (!req.body) return;
const contentLength = req.headers.get("content-length");
if (contentLength) {
if (req.headers.get("transfer-encoding")) throw new HTTPError({ status: 400 });
if (+contentLength > limit) throw bodyTooLargeError(limit);
}
event.req = limitRequestBody(req, limit, { createError: () => bodyTooLargeError(limit) });
}
function bodyTooLargeError(limit) {
return new HTTPError({
status: 413,
statusText: "Request Entity Too Large",
message: `Request body size exceeds the limit of ${limit} bytes`
});
}
function onRequest(hook) {
return async function _onRequestMiddleware(event) {
await hook(event);
};
}
function onResponse(hook) {
return async function _onResponseMiddleware(event, next) {
const rawBody = await next();
const response = await toResponse(rawBody, event);
return await hook(response, event) || response;
};
}
function onError(hook) {
return async (event, next) => {
try {
return await next();
} catch (rawError) {
const isHTTPError = HTTPError.isError(rawError);
const error = isHTTPError ? rawError : new HTTPError(rawError);
if (!isHTTPError) {
error.unhandled = true;
if (rawError?.stack) error.stack = rawError.stack;
}
const hookResponse = await hook(error, event);
if (hookResponse !== void 0) return hookResponse;
throw error;
}
};
}
function bodyLimit(limit) {
return (event, next) => {
assertBodySize(event, limit);
return next();
};
}
const COOKIE_MAX_AGE_LIMIT = 3456e4;
function endIndex(str, min, len) {
const index = str.indexOf(";", min);
return index === -1 ? len : index;
}
function eqIndex(str, min, max) {
const index = str.indexOf("=", min);
return index < max ? index : -1;
}
function valueSlice(str, min, max) {
if (min === max) return "";
let start = min;
let end = max;
do {
const code = str.charCodeAt(start);
if (code !== 32 && code !== 9) break;
} while (++start < end);
while (end > start) {
const code = str.charCodeAt(end - 1);
if (code !== 32 && code !== 9) break;
end--;
}
return str.slice(start, end);
}
const NullObject = /* @__PURE__ */ (() => {
const C = function() {};
C.prototype = Object.create(null);
return C;
})();
function parse(str, options) {
const obj = new NullObject();
const len = str.length;
if (len < 2) return obj;
const dec = options?.decode || decode;
const allowMultiple = options?.allowMultiple || false;
let index = 0;
do {
const eqIdx = eqIndex(str, index, len);
if (eqIdx === -1) break;
const endIdx = endIndex(str, index, len);
if (eqIdx > endIdx) {
index = str.lastIndexOf(";", eqIdx - 1) + 1;
continue;
}
const key = valueSlice(str, index, eqIdx);
if (options?.filter && !options.filter(key)) {
index = endIdx + 1;
continue;
}
const val = dec(valueSlice(str, eqIdx + 1, endIdx));
if (allowMultiple) {
const existing = obj[key];
if (existing === void 0) obj[key] = val;
else if (Array.isArray(existing)) existing.push(val);
else obj[key] = [existing, val];
} else if (obj[key] === void 0) obj[key] = val;
index = endIdx + 1;
} while (index < len);
return obj;
}
function decode(str) {
if (!str.includes("%")) return str;
try {
return decodeURIComponent(str);
} catch {
return str;
}
}
const cookieNameRegExp = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/;
const cookieValueRegExp = /^[\u0021-\u003A\u003C-\u007E]*$/;
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;
const pathValueRegExp = /^[\u0020-\u003A\u003C-\u007E]*$/;
const __toString = Object.prototype.toString;
function serialize(_a0, _a1, _a2) {
const isObj = typeof _a0 === "object" && _a0 !== null;
const options = isObj ? _a1 : _a2;
const stringify = options?.stringify || JSON.stringify;
const cookie = isObj ? _a0 : {
..._a2,
name: _a0,
value: _a1 == void 0 ? "" : typeof _a1 === "string" ? _a1 : stringify(_a1)
};
const enc = options?.encode || encodeURIComponent;
if (!cookieNameRegExp.test(cookie.name)) throw new TypeError(`argument name is invalid: ${cookie.name}`);
const value = cookie.value ? enc(cookie.value) : "";
if (!cookieValueRegExp.test(value)) throw new TypeError(`argument val is invalid: ${cookie.value}`);
if (!cookie.secure) {
if (cookie.partitioned) throw new TypeError(`Partitioned cookies must have the Secure attribute`);
if (cookie.sameSite && String(cookie.sameSite).toLowerCase() === "none") throw new TypeError(`SameSite=None cookies must have the Secure attribute`);
if (cookie.name.length > 9 && cookie.name.charCodeAt(0) === 95 && cookie.name.charCodeAt(1) === 95) {
const nameLower = cookie.name.toLowerCase();
if (nameLower.startsWith("__secure-") || nameLower.startsWith("__host-")) throw new TypeError(`${cookie.name} cookies must have the Secure attribute`);
}
}
if (cookie.name.length > 7 && cookie.name.charCodeAt(0) === 95 && cookie.name.charCodeAt(1) === 95 && cookie.name.toLowerCase().startsWith("__host-")) {
if (cookie.path !== "/") throw new TypeError(`__Host- cookies must have Path=/`);
if (cookie.domain) throw new TypeError(`__Host- cookies must not have a Domain attribute`);
}
let str = cookie.name + "=" + value;
if (cookie.maxAge !== void 0) {
if (!Number.isInteger(cookie.maxAge)) throw new TypeError(`option maxAge is invalid: ${cookie.maxAge}`);
str += "; Max-Age=" + Math.max(0, Math.min(cookie.maxAge, COOKIE_MAX_AGE_LIMIT));
}
if (cookie.domain) {
if (!domainValueRegExp.test(cookie.domain)) throw new TypeError(`option domain is invalid: ${cookie.domain}`);
str += "; Domain=" + cookie.domain;
}
if (cookie.path) {
if (!pathValueRegExp.test(cookie.path)) throw new TypeError(`option path is invalid: ${cookie.path}`);
str += "; Path=" + cookie.path;
}
if (cookie.expires) {
if (!isDate(cookie.expires) || !Number.isFinite(cookie.expires.valueOf())) throw new TypeError(`option expires is invalid: ${cookie.expires}`);
str += "; Expires=" + cookie.expires.toUTCString();
}
if (cookie.httpOnly) str += "; HttpOnly";
if (cookie.secure) str += "; Secure";
if (cookie.partitioned) str += "; Partitioned";
if (cookie.priority) switch (typeof cookie.priority === "string" ? cookie.priority.toLowerCase() : void 0) {
case "low":
str += "; Priority=Low";
break;
case "medium":
str += "; Priority=Medium";
break;
case "high":
str += "; Priority=High";
break;
default: throw new TypeError(`option priority is invalid: ${cookie.priority}`);
}
if (cookie.sameSite) switch (typeof cookie.sameSite === "string" ? cookie.sameSite.toLowerCase() : cookie.sameSite) {
case true:
case "strict":
str += "; SameSite=Strict";
break;
case "lax":
str += "; SameSite=Lax";
break;
case "none":
str += "; SameSite=None";
break;
default: throw new TypeError(`option sameSite is invalid: ${cookie.sameSite}`);
}
return str;
}
function isDate(val) {
return __toString.call(val) === "[object Date]";
}
const maxAgeRegExp = /^-?\d+$/;
const _nullProto = /* @__PURE__ */ Object.getPrototypeOf({});
function parseSetCookie(str, options) {
const len = str.length;
let _endIdx = len;
let eqIdx = -1;
for (let i = 0; i < len; i++) {
const c = str.charCodeAt(i);
if (c === 59) {
_endIdx = i;
break;
}
if (c === 61 && eqIdx === -1) eqIdx = i;
}
if (eqIdx >= _endIdx) eqIdx = -1;
const name = eqIdx === -1 ? "" : _trim(str, 0, eqIdx);
if (name && name in _nullProto) return void 0;
let value = eqIdx === -1 ? _trim(str, 0, _endIdx) : _trim(str, eqIdx + 1, _endIdx);
if (!name && !value) return void 0;
if (name.length + value.length > 4096) return void 0;
if (options?.decode !== false) value = _decode(value, options?.decode);
const setCookie = {
name,
value
};
let index = _endIdx + 1;
while (index < len) {
let endIdx = len;
let attrEqIdx = -1;
for (let i = index; i < len; i++) {
const c = str.charCodeAt(i);
if (c === 59) {
endIdx = i;
break;
}
if (c === 61 && attrEqIdx === -1) attrEqIdx = i;
}
if (attrEqIdx >= endIdx) attrEqIdx = -1;
const attr = attrEqIdx === -1 ? _trim(str, index, endIdx) : _trim(str, index, attrEqIdx);
const val = attrEqIdx === -1 ? void 0 : _trim(str, attrEqIdx + 1, endIdx);
if (val === void 0 || val.length <= 1024) switch (attr.toLowerCase()) {
case "httponly":
setCookie.httpOnly = true;
break;
case "secure":
setCookie.secure = true;
break;
case "partitioned":
setCookie.partitioned = true;
break;
case "domain":
if (val) setCookie.domain = (val.charCodeAt(0) === 46 ? val.slice(1) : val).toLowerCase();
break;
case "path":
setCookie.path = val;
break;
case "max-age":
if (val && maxAgeRegExp.test(val)) setCookie.maxAge = Math.min(Number(val), COOKIE_MAX_AGE_LIMIT);
break;
case "expires": {
if (!val) break;
const date = new Date(val);
if (Number.isFinite(date.valueOf())) {
const maxDate = new Date(Date.now() + COOKIE_MAX_AGE_LIMIT * 1e3);
setCookie.expires = date > maxDate ? maxDate : date;
}
break;
}
case "priority": {
if (!val) break;
const priority = val.toLowerCase();
if (priority === "low" || priority === "medium" || priority === "high") setCookie.priority = priority;
break;
}
case "samesite": {
if (!val) break;
const sameSite = val.toLowerCase();
if (sameSite === "lax" || sameSite === "strict" || sameSite === "none") setCookie.sameSite = sameSite;
else setCookie.sameSite = "lax";
break;
}
default: {
const attrLower = attr.toLowerCase();
if (attrLower && !(attrLower in _nullProto)) setCookie[attrLower] = val;
}
}
index = endIdx + 1;
}
return setCookie;
}
function _trim(str, start, end) {
if (start === end) return "";
let s = start;
let e = end;
while (s < e && (str.charCodeAt(s) === 32 || str.charCodeAt(s) === 9)) s++;
while (e > s && (str.charCodeAt(e - 1) === 32 || str.charCodeAt(e - 1) === 9)) e--;
return str.slice(s, e);
}
function _decode(value, decode) {
if (!decode && !value.includes("%")) return value;
try {
return (decode || decodeURIComponent)(value);
} catch {
return value;
}
}
const CHUNKED_COOKIE = "__chunked__";
const CHUNKS_MAX_LENGTH = 4e3;
function parseCookies(event) {
return parse(event.req.headers.get("cookie") || "");
}
function getValidatedCookies(event, validate, options) {
const cookies = parseCookies(event);
return validateData(cookies, validate, options);
}
function getCookie(event, name) {
return parseCookies(event)[name];
}
function setCookie(event, name, value, options) {
const { encode, stringify, ...attrs } = options ?? {};
const newCookie = serialize({
name,
value,
path: "/",
...attrs
}, {
encode,
stringify
});
const currentCookies = event.res.headers.getSetCookie();
if (currentCookies.length === 0) {
event.res.headers.set("set-cookie", newCookie);
return;
}
const namePrefix = `${name}=`;
if (!currentCookies.some((cookie) => cookie.startsWith(namePrefix))) {
event.res.headers.append("set-cookie", newCookie);
return;
}
const newCookieKey = _getDistinctCookieKey(name, options || {});
event.res.headers.delete("set-cookie");
for (const cookie of currentCookies) {
const parsed = parseSetCookie(cookie);
if (parsed ? _getDistinctCookieKey(cookie.split("=")?.[0], parsed) === newCookieKey : cookie.startsWith(namePrefix)) continue;
event.res.headers.append("set-cookie", cookie);
}
event.res.headers.append("set-cookie", newCookie);
}
function deleteCookie(event, name, serializeOptions) {
setCookie(event, name, "", {
...serializeOptions,
maxAge: 0
});
}
function getChunkedCookie(event, name) {
const cookies = parseCookies(event);
const mainCookie = cookies[name];
if (!mainCookie || !mainCookie.startsWith(CHUNKED_COOKIE)) return mainCookie;
const chunksCount = getChunkedCookieCount(mainCookie);
if (chunksCount === 0) return;
const chunks = [];
for (let i = 1; i <= chunksCount; i++) {
const chunk = cookies[chunkCookieName(name, i)];
if (!chunk) return;
chunks.push(chunk);
}
return chunks.join("");
}
function setChunkedCookie(event, name, value, options) {
const chunkMaxLength = options?.chunkMaxLength || CHUNKS_MAX_LENGTH;
const chunkCount = Math.ceil(value.length / chunkMaxLength);
if (chunkCount > MAX_CHUNKED_COOKIE_COUNT) throw new HTTPError({
status: 500,
message: `Cannot set chunked cookie "${name}": value needs ${chunkCount} chunks, exceeding the maximum of ${MAX_CHUNKED_COOKIE_COUNT}.`
});
const previousCookie = getCookie(event, name);
if (previousCookie?.startsWith(CHUNKED_COOKIE)) {
const previousChunkCount = getChunkedCookieCount(previousCookie);
const newChunkCount = chunkCount <= 1 ? 0 : chunkCount;
for (let i = newChunkCount + 1; i <= previousChunkCount; i++) deleteCookie(event, chunkCookieName(name, i), options);
}
if (chunkCount <= 1) {
setCookie(event, name, value, options);
return;
}
setCookie(event, name, `${CHUNKED_COOKIE}${chunkCount}`, options);
for (let i = 1; i <= chunkCount; i++) {
const start = (i - 1) * chunkMaxLength;
const end = start + chunkMaxLength;
const chunkValue = value.slice(start, end);
setCookie(event, chunkCookieName(name, i), chunkValue, options);
}
}
function deleteChunkedCookie(event, name, serializeOptions) {
const mainCookie = getCookie(event, name);
deleteCookie(event, name, serializeOptions);
const chunksCount = getChunkedCookieCount(mainCookie);
if (chunksCount >= 0) for (let i = 0; i < chunksCount; i++) deleteCookie(event, chunkCookieName(name, i + 1), serializeOptions);
}
function _getDistinctCookieKey(name, options) {
return [
name,
(options.domain || "").replace(/^\./, "").toLowerCase(),
options.path || "/"
].join(";");
}
const MAX_CHUNKED_COOKIE_COUNT = 100;
function getChunkedCookieCount(cookie) {
if (!cookie?.startsWith(CHUNKED_COOKIE)) return NaN;
const count = Number.parseInt(cookie.slice(11));
if (Number.isNaN(count) || count < 0 || count > MAX_CHUNKED_COOKIE_COUNT) return NaN;
return count;
}
function chunkCookieName(name, chunkNumber) {
return `${name}.${chunkNumber}`;
}
function formatEventStreamComment(comment) {
return comment.split(/\r\n|\r|\n/).map((l) => `: ${l}\n`).join("") + "\n";
}
function formatEventStreamMessage(message) {
let result = "";
if (message.id) result += `id: ${_sanitizeSingleLine(message.id)}\n`;
if (message.event) result += `event: ${_sanitizeSingleLine(message.event)}\n`;
if (typeof message.retry === "number" && Number.isInteger(message.retry)) result += `retry: ${message.retry}\n`;
const data = typeof message.data === "string" ? message.data : "";
for (const line of data.split(/\r\n|\r|\n/)) result += `data: ${line}\n`;
result += "\n";
return result;
}
function _sanitizeSingleLine(value) {
return value.replace(/[\n\r]/g, "");
}
function formatEventStreamMessages(messages) {
let result = "";
for (const msg of messages) result += formatEventStreamMessage(msg);
return result;
}
function eventStreamHeaders(event) {
const headers = {
"content-type": "text/event-stream",
"cache-control": "private, no-cache, no-store, no-transform, must-revalidate, max-age=0",
"x-accel-buffering": "no"
};
if (event.req.headers.get("connection") === "keep-alive") headers["connection"] = "keep-alive";
return headers;
}
function setEventStreamHeaders(event) {
for (const [name, value] of Object.entries(eventStreamHeaders(event))) event.res.headers.set(name, value);
}
const _noop = () => {};
var EventStream = class extends HTTPResponse {
_event;
_transformStream;
_writer;
_encoder = new TextEncoder();
_closeCallbacks = [];
_writerIsClosed = false;
_paused = false;
_unsentData;
_disposed = false;
get _isClosed() {
return this._writerIsClosed || this._disposed;
}
constructor(event, _opts = {}) {
const transformStream = new TransformStream();
super(transformStream.readable, {
status: 200,
headers: eventStreamHeaders(event)
});
this._event = event;
this._transformStream = transformStream;
this._writer = transformStream.writable.getWriter();
this._writer.closed.catch(_noop).finally(() => {
this._writerIsClosed = true;
this._disposed = true;
for (const cb of this._closeCallbacks.splice(0)) _invokeCloseCallback(cb);
});
onDispose(this._event, () => this.close());
}
async push(message) {
if (typeof message === "string") {
await this._sendEvent({ data: message });
return;
}
if (Array.isArray(message)) {
if (message.length === 0) return;
if (typeof message[0] === "string") {
const msgs = [];
for (const item of message) msgs.push({ data: item });
await this._sendEvents(msgs);
return;
}
await this._sendEvents(message);
return;
}
await this._sendEvent(message);
}
async pushComment(comment) {
if (this._isClosed) return;
if (this._paused && !this._unsentData) {
this._unsentData = formatEventStreamComment(comment);
return;
}
if (this._paused) {
this._unsentData += formatEventStreamComment(comment);
return;
}
await this._writer.write(this._encoder.encode(formatEventStreamComment(comment))).catch(() => {
this._writerIsClosed = true;
});
}
async _sendEvent(message) {
if (this._isClosed) return;
if (this._paused && !this._unsentData) {
this._unsentData = formatEventStreamMessage(message);
return;
}
if (this._paused) {
this._unsentData += formatEventStreamMessage(message);
return;
}
await this._writer.write(this._encoder.encode(formatEventStreamMessage(message))).catch(() => {
this._writerIsClosed = true;
});
}
async _sendEvents(messages) {
if (this._isClosed) return;
const payload = formatEventStreamMessages(messages);
if (this._paused && !this._unsentData) {
this._unsentData = payload;
return;
}
if (this._paused) {
this._unsentData += payload;
return;
}
await this._writer.write(this._encoder.encode(payload)).catch(() => {
this._writerIsClosed = true;
});
}
pause() {
this._paused = true;
}
get isPaused() {
return this._paused;
}
async resume() {
this._paused = false;
await this.flush();
}
async flush() {
if (this._isClosed) return;
if (this._unsentData?.length) {
await this._writer.write(this._encoder.encode(this._unsentData)).catch(() => {
this._writerIsClosed = true;
});
this._unsentData = void 0;
}
}
async close() {
if (this._disposed) return;
if (!this._isClosed) {
this._paused = false;
await this.flush();
try {
await this._writer.close();
} catch {}
}
this._disposed = true;
}
onClosed(cb) {
if (this._writerIsClosed) {
queueMicrotask(() => _invokeCloseCallback(cb));
return;
}
this._closeCallbacks.push(cb);
}
async send() {
setEventStreamHeaders(this._event);
this._event.res.status = 200;
return this._transformStream.readable;
}
};
function _invokeCloseCallback(cb) {
try {
const res = cb();
if (res instanceof Promise) res.catch(_noop);
} catch {}
}
function setServerTiming(event, name, opts) {
if (!_isValidToken(name)) throw new TypeError(`Invalid Server-Timing metric name: ${name}`);
if (opts?.dur !== void 0 && (!Number.isFinite(opts.dur) || opts.dur < 0)) throw new TypeError(`Invalid Server-Timing duration: ${opts.dur}`);
const value = name + (opts?.desc ? `;desc="${_escapeDesc(opts.desc)}"` : "") + (opts?.dur !== void 0 ? `;dur=${opts.dur}` : "");
event.res.headers.append("server-timing", value);
const ctx = event.context;
if (!Array.isArray(ctx.timing)) ctx.timing = [];
ctx.timing.push({
name,
...opts
});
}
async function withServerTiming(event, name, fn) {
const start = performance.now();
try {
return await fn();
} finally {
setServerTiming(event, name, { dur: performance.now() - start });
}
}
const _tokenRE = /^[\w!#$%&'*+.^`|~-]+$/;
function _isValidToken(value) {
return _tokenRE.test(value);
}
function _escapeDesc(value) {
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
}
const COMMON_MIME_TYPES = {
".html": "text/html",
".htm": "text/html",
".css": "text/css",
".js": "text/javascript",
".json": "application/json",
".txt": "text/plain",
".xml": "application/xml",
".gif": "image/gif",
".ico": "image/vnd.microsoft.icon",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".png": "image/png",
".svg": "image/svg+xml",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
".mp4": "video/mp4",
".webm": "video/webm",
".zip": "application/zip",
".pdf": "application/pdf"
};
function getExtension(path) {
const filename = path.split("/").pop();
if (!filename) return;
const separatorIndex = filename.lastIndexOf(".");
if (separatorIndex !== -1) return filename.slice(separatorIndex);
}
function getType(ext) {
return ext ? COMMON_MIME_TYPES[ext] : void 0;
}
async function serveStatic(event, options) {
if (options.headers) {
const entries = Array.isArray(options.headers) ? options.headers : typeof options.headers.entries === "function" ? options.headers.entries() : Object.entries(options.headers);
for (const [key, value] of entries) event.res.headers.set(key, value);
}
if (event.req.method !== "GET" && event.req.method !== "HEAD") {
if (options.fallthrough) return;
event.res.headers.set("allow", "GET, HEAD");
throw new HTTPError({ status: 405 });
}
if (!isCanonicalPath(event.url.pathname)) {
if (options.fallthrough) return;
throw new HTTPError({ status: 404 });
}
const resolvedId = withoutTrailingSlash(event.url.pathname);
let originalId = resolvedId;
if (resolvedId.includes("%")) try {
const decodedId = decodePreservingSeparators(resolvedId, {
decode: decodeURI,
nested: false
});
if (isCanonicalPath(decodedId)) originalId = withoutTrailingSlash(decodedId);
} catch {}
const acceptEncodings = parseAcceptEncoding(event.req.headers.get("accept-encoding") || "", options.encodings);
if (acceptEncodings.length > 1) event.res.headers.set("vary", "accept-encoding");
let id = originalId;
let meta;
const _ids = idSearchPaths(originalId, acceptEncodings, options.indexNames || ["/index.html"]);
for (const _id of _ids) {
const _meta = await options.getMeta(_id);
if (_meta) {
meta = _meta;
id = _id;
break;
}
}
if (!meta) {
if (options.fallthrough) return;
throw new HTTPError({ statusCode: 404 });
}
let mtimeDate;
if (meta.mtime) {
mtimeDate = new Date(meta.mtime);
mtimeDate.setMilliseconds(0);
if (!event.res.headers.get("last-modified")) event.res.headers.set("last-modified", mtimeDate.toUTCString());
}
if (meta.etag && !event.res.headers.has("etag")) event.res.headers.set("etag", meta.etag);
if (isCacheMatch(event.req.headers, {
etag: meta.etag,
lastModified: mtimeDate
})) return new HTTPResponse(null, {
status: 304,
statusText: "Not Modified"
});
if (!event.res.headers.get("content-type")) {
if (meta.type) event.res.headers.set("content-type", meta.type);
else {
const ext = getExtension(id);
const type = ext ? options.getType?.(ext) ?? getType(ext) : void 0;
if (type) event.res.headers.set("content-type", type);
}
}
if (meta.encoding && !event.res.headers.get("content-encoding")) event.res.headers.set("content-encoding", meta.encoding);
if (meta.size !== void 0 && meta.size > 0 && !event.res.headers.get("content-length")) event.res.headers.set("content-length", meta.size + "");
if (event.req.method === "HEAD") return new HTTPResponse(null, { status: 200 });
const contents = await options.getContents(id);
return new HTTPResponse(contents || null, { status: 200 });
}
function parseAcceptEncoding(header, encodingMap) {
if (!encodingMap || !header) return [];
return String(header || "").split(",").map((e) => encodingMap[e.trim()]).filter(Boolean);
}
function idSearchPaths(id, encodings, indexNames) {
const ids = [];
for (const suffix of ["", ...indexNames]) for (const encoding of [...encodings, ""]) ids.push(`${id}${suffix}${encoding}`);
return ids;
}
function withBase(base, input) {
base = withoutTrailingSlash(base);
const handler = toEventHandler(input);
if (!handler) throw new Error("Invalid handler", { cause: input });
return async function _handlerWithBase(event) {
const _pathBefore = event.url.pathname || "/";
event.url.pathname = withoutBase(event.url.pathname || "/", base);
try {
return await handler(event);
} finally {
event.url.pathname = _pathBefore;
}
};
}
const defaults = /* @__PURE__ */ Object.freeze({
ttl: 0,
timestampSkewSec: 60,
localtimeOffsetMsec: 0,
encryption: /* @__PURE__ */ Object.freeze({
saltBits: 256,
algorithm: "aes-256-cbc",
iterations: 8192,
minPasswordlength: 32
}),
integrity: /* @__PURE__ */ Object.freeze({
saltBits: 256,
algorithm: "sha256",
iterations: 8192,
minPasswordlength: 32
})
});
const algorithms = /* @__PURE__ */ Object.freeze({
"aes-128-ctr": /* @__PURE__ */ Object.freeze({
keyBits: 128,
ivBits: 128,
name: "AES-CTR"
}),
"aes-256-cbc": /* @__PURE__ */ Object.freeze({
keyBits: 256,
ivBits: 128,
name: "AES-CBC"
}),
sha256: /* @__PURE__ */ Object.freeze({
keyBits: 256,
ivBits: 128,
name: "SHA-256"
})
});
const macPrefix = "Fe26.2";
async function seal(object, password, opts) {
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
if (!password) throw new Error("Empty password");
const { id = "", encryption, integrity } = normalizePassword(password);
if (id && !/^\w+$/.test(id)) throw new Error("Invalid password id");
const { encrypted, key } = await encrypt(encryption, opts.encryption, JSON.stringify(object));
const encryptedB64 = base64Encode(encrypted);
const iv = base64Encode(key.iv);
const expiration = opts.ttl ? now + opts.ttl : "";
const macBaseString = `${macPrefix}*${id}*${key.salt}*${iv}*${encryptedB64}*${expiration}`;
const mac = await hmacWithPassword(integrity, opts.integrity, macBaseString);
return `${macBaseString}*${mac.salt}*${mac.digest}`;
}
async function unseal(sealed, password, opts) {
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
if (!password) throw new Error("Empty password");
const parts = sealed.split("*");
if (parts.length !== 8) throw new Error("Incorrect number of sealed components");
const [prefix, passwordId, encryptionSalt, encryptionIv, encryptedB64, expiration, hmacSalt, hmac] = parts;
const macBaseString = `${prefix}*${passwordId}*${encryptionSalt}*${encryptionIv}*${encryptedB64}*${expiration}`;
if ("Fe26.2" !== prefix) throw new Error("Wrong mac prefix");
if (expiration) {
if (!/^\d+$/.test(expiration)) throw new Error("Invalid expiration");
if (Number.parseInt(expiration, 10) <= now - opts.timestampSkewSec * 1e3) throw new Error("Expired seal");
}
let pass = "";
const _passwordId = passwordId || "default";
if (typeof password === "string" || password instanceof Uint8Array) pass = password;
else if (_passwordId in password) pass = password[_passwordId];
else throw new Error(`Cannot find password: ${_passwordId}`);
pass = normalizePassword(pass);
if (!fixedTimeComparison((await hmacWithPassword(pass.integrity, {
...opts.integrity,
salt: hmacSalt
}, macBaseString)).digest, hmac)) throw new Error("Bad hmac value");
const encrypted = base64Decode(encryptedB64);
const decryptOptions = {
...opts.encryption,
salt: encryptionSalt,
iv: base64Decode(encryptionIv)
};
const decrypted = await decrypt(pass.encryption, decryptOptions, encrypted);
return decrypted ? JSON.parse(decrypted) : null;
}
async function hmacWithPassword(password, options, data) {
const key = await generateKey(password, {
...options,
hmac: true
});
const textBuffer = textEncoder.encode(data);
const signed = await crypto.subtle.sign({ name: "HMAC" }, key.key, textBuffer);
return {
digest: base64Encode(new Uint8Array(signed)),
salt: key.salt
};
}
async function generateKey(password, options) {
if (!password?.length) throw new Error("Empty password");
if (options == null || typeof options !== "object") throw new Error("Bad options");
if (!(options.algorithm in algorithms)) throw new Error(`Unknown algorithm: ${options.algorithm}`);
const algorithm = algorithms[options.algorithm];
let resultKey;
let resultSalt;
let resultIV;
const hmac = options.hmac ?? false;
const id = hmac ? {
name: "HMAC",
hash: algorithm.name
} : { name: algorithm.name };
const usage = hmac ? ["sign", "verify"] : ["encrypt", "decrypt"];
if (typeof password === "string") {
if (password.length < options.minPasswordlength) throw new Error(`Password string too short (min ${options.minPasswordlength} characters required)`);
let { salt = "" } = options;
if (!salt) {
const { saltBits = 0 } = options;
if (!saltBits) throw new Error("Missing salt and saltBits options");
const randomSalt = randomBits(saltBits);
salt = [...new Uint8Array(randomSalt)].map((x) => x.toString(16).padStart(2, "0")).join("");
}
const derivedKey = await pbkdf2(password, salt, options.iterations, algorithm.keyBits / 8, "SHA-1");
resultKey = await crypto.subtle.importKey("raw", derivedKey, id, false, usage);
resultSalt = salt;
} else {
if (password.length < algorithm.keyBits / 8) throw new Error("Key buffer (password) too small");
resultKey = await crypto.subtle.importKey("raw", password, id, false, usage);
resultSalt = "";
}
if (options.iv) resultIV = options.iv;
else if ("ivBits" in algorithm) resultIV = randomBits(algorithm.ivBits);
else throw new Error("Missing IV");
return {
key: resultKey,
salt: resultSalt,
iv: resultIV
};
}
async function pbkdf2(password, salt, iterations, keyLength, hash) {
const passwordBuffer = textEncoder.encode(password);
const importedKey = await crypto.subtle.importKey("raw", passwordBuffer, { name: "PBKDF2" }, false, ["deriveBits"]);
const params = {
name: "PBKDF2",
hash,
salt: textEncoder.encode(salt),
iterations
};
return await crypto.subtle.deriveBits(params, importedKey, keyLength * 8);
}
async function encrypt(password, options, data) {
const key = await generateKey(password, options);
const encrypted = await crypto.subtle.encrypt(...getEncryptParams(options.algorithm, key, data));
return {
encrypted: new Uint8Array(encrypted),
key
};
}
async function decrypt(password, options, data) {
const key = await generateKey(password, options);
const decrypted = await crypto.subtle.decrypt(...getEncryptParams(options.algorithm, key, data));
return textDecoder.decode(decrypted);
}
function getEncryptParams(algorithm, key, data) {
return [
algorithm === "aes-128-ctr" ? {
name: "AES-CTR",
counter: key.iv,
length: 128
} : {
name: "AES-CBC",
iv: key.iv
},
key.key,
typeof data === "string" ? textEncoder.encode(data) : data
];
}
function fixedTimeComparison(a, b) {
let mismatch = a.length === b.length ? 0 : 1;
if (mismatch) b = a;
for (let i = 0; i < a.length; i += 1) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
return mismatch === 0;
}
function normalizePassword(password) {
if (typeof password === "string" || password instanceof Uint8Array) return {
encryption: password,
integrity: password
};
if ("secret" in password) return {
id: password.id,
encryption: password.secret,
integrity: password.secret
};
return {
id: password.id,
encryption: password.encryption,
integrity: password.integrity
};
}
function randomBits(bits) {
if (bits < 1) throw new Error("Invalid random bits count");
return randomBytes(Math.ceil(bits / 8));
}
function randomBytes(size) {
const bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
return bytes;
}
const kGetSession = /* @__PURE__ */ Symbol.for("h3.internal.session.promise");
const kSessionNew = /* @__PURE__ */ Symbol.for("h3.internal.session.new");
const kLegacySeal = /* @__PURE__ */ Symbol.for("h3.internal.session.legacy-seal");
const DEFAULT_SESSION_COOKIE = {
path: "/",
secure: true,
httpOnly: true,
sameSite: "lax"
};
async function useSession(event, config) {
const sessionName = config.name || "h3";
if ((await getSession(event, config))[kSessionNew]) await updateSession(event, config);
const sessionManager = {
get id() {
return getEventContext(event)?.sessions?.[sessionName]?.id;
},
get data() {
return getEventContext(event).sessions?.[sessionName]?.data || {};
},
update: async (update) => {
await updateSession(event, config, update);
return sessionManager;
},
clear: () => {
clearSession(event, config);
return Promise.resolve(sessionManager);
}
};
return sessionManager;
}
async function getSession(event, config) {
const sessionName = config.name || "h3";
const context = getEventContext(event);
if (!context.sessions) context.sessions = new EmptyObject();
const existingSession = context.sessions[sessionName];
if (existingSession) return existingSession[kGetSession] || existingSession;
const session = {
id: "",
createdAt: 0,
data: new EmptyObject()
};
context.sessions[sessionName] = session;
let sealedSession;
if (config.sessionHeader !== false) {
const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
const headerValue = event.req.headers.get(headerName);
if (typeof headerValue === "string") sealedSession = headerValue;
}
let sessionFromCookie = false;
if (!sealedSession) {
sealedSession = getChunkedCookie(event, sessionName);
sessionFromCookie = true;
}
if (sealedSession) {
const promise = unsealSession(event, config, sealedSession).catch(() => {}).then(async (unsealed) => {
const legacySeal = unsealed && unsealed[kLegacySeal];
if (legacySeal) delete unsealed[kLegacySeal];
Object.assign(session, unsealed);
delete context.sessions[sessionName][kGetSession];
if (session.id && sessionFromCookie && (legacySeal || shouldSlide(session, config))) await updateSession(event, config);
return session;
});
context.sessions[sessionName][kGetSession] = promise;
await promise;
}
if (!session.id) {
session.id = config.generateId?.() ?? (config.crypto || crypto).randomUUID();
session.createdAt = Date.now();
session[kSessionNew] = true;
}
return session;
}
async function updateSession(event, config, update) {
const sessionName = config.name || "h3";
const session = getEventContext(event).sessions?.[sessionName] || await getSession(event, config);
if (typeof update === "function") update = update(session.data);
if (update) Object.assign(session.data, update);
delete session[kSessionNew];
if (config.cookie !== false && event.res) {
setChunkedCookie(event, sessionName, await sealSession(event, config), {
...DEFAULT_SESSION_COOKIE,
expires: sessionExpires(session, config),
...config.cookie
});
stageSessionErrCookies(event, sessionName);
}
return session;
}
async function sealSession(event, config) {
const sessionName = config.name || "h3";
const session = getEventContext(event).sessions?.[sessionName] || await getSession(event, config);
if (config.idleTimeout) session.lastSeenAt = Date.now();
return await seal(session, config.password, {
...defaults,
ttl: (config.maxAge || config.idleTimeout || 0) * 1e3,
...config.seal
});
}
async function unsealSession(_event, config, sealed) {
const sealOptions = {
...defaults,
ttl: (config.maxAge || config.idleTimeout || 0) * 1e3,
...config.seal
};
let unsealed;
try {
unsealed = await unseal(sealed, config.password, sealOptions);
} catch (error) {
if (config.legacySealFallback === false || sealOptions.integrity.iterations === 1 || !(error instanceof Error) || error.message !== "Bad hmac value") throw error;
unsealed = await unseal(sealed, config.password, {
...sealOptions,
encryption: {
...sealOptions.encryption,
iterations: 1
},
integrity: {
...sealOptions.integrity,
iterations: 1
}
});
if (unsealed) unsealed[kLegacySeal] = true;
}
if (config.maxAge) {
if (Date.now() - (unsealed.createdAt || Number.NEGATIVE_INFINITY) > config.maxAge * 1e3) throw new Error("Session expired!");
}
if (config.idleTimeout) {
if (Date.now() - (unsealed.lastSeenAt || unsealed.createdAt || Number.NEGATIVE_INFINITY) > config.idleTimeout * 1e3) throw new Error("Session expired!");
}
return unsealed;
}
function clearSession(event, config) {
const context = getEventContext(event);
const sessionName = config.name || "h3";
if (context.sessions?.[sessionName]) delete context.sessions[sessionName];
if (event.res && config.cookie !== false) {
deleteChunkedCookie(event, sessionName, {
...DEFAULT_SESSION_COOKIE,
...config.cookie
});
stageSessionErrCookies(event, sessionName);
}
return Promise.resolve();
}
const SLIDE_THRESHOLD = .5;
function shouldSlide(session, config) {
if (!config.idleTimeout) return false;
const lastSeenAt = session.lastSeenAt || session.createdAt || 0;
return Date.now() - lastSeenAt > config.idleTimeout * 1e3 * SLIDE_THRESHOLD;
}
function stageSessionErrCookies(event, sessionName) {
const isSessionCookie = (cookie) => cookie.startsWith(`${sessionName}=`) || cookie.startsWith(`${sessionName}.`);
const errHeaders = event.res.errHeaders;
const staged = [...errHeaders.getSetCookie().filter((cookie) => !isSessionCookie(cookie)), ...event.res.headers.getSetCookie().filter((cookie) => isSessionCookie(cookie))];
errHeaders.delete("set-cookie");
for (const cookie of staged) errHeaders.append("set-cookie", cookie);
}
function sessionExpires(session, config) {
const times = [];
if (config.maxAge) times.push(session.createdAt + config.maxAge * 1e3);
if (config.idleTimeout) times.push((session.lastSeenAt || session.createdAt) + config.idleTimeout * 1e3);
return times.length > 0 ? new Date(Math.min(...times)) : void 0;
}
const _textEncoder = /* @__PURE__ */ new TextEncoder();
function timingSafeEqual(a, b) {
const aBuf = _textEncoder.encode(a);
const bBuf = _textEncoder.encode(b);
const aLen = aBuf.length;
const bLen = bBuf.length;
const len = Math.max(aLen, bLen);
let result = aLen === bLen ? 0 : 1;
for (let i = 0; i < len; i++) result |= (aBuf[i % aLen] ?? 0) ^ (bBuf[i % bLen] ?? 0);
return result === 0;
}
function randomJitter() {
const randomBuffer = /* @__PURE__ */ new Uint32Array(1);
crypto.getRandomValues(randomBuffer);
const jitter = randomBuffer[0] % 100;
return new Promise((resolve) => setTimeout(resolve, jitter));
}
async function requireBasicAuth(event, opts) {
if (!opts.validate && !opts.password) throw new HTTPError({
message: "Either 'password' or 'validate' option must be provided",
status: 500
});
const realm = opts?.realm ?? "auth";
const authHeader = event.req.headers.get("authorization");
if (!authHeader) throw await authFailed(event, realm);
const b64auth = /^basic +(.+)$/i.exec(authHeader)?.[1];
if (!b64auth) throw await authFailed(event, realm);
let authDecoded;
try {
authDecoded = atob(b64auth);
} catch {
throw await authFailed(event, realm);
}
try {
authDecoded = new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(authDecoded, (c) => c.charCodeAt(0)));
} catch {}
const colonIndex = authDecoded.indexOf(":");
if (colonIndex === -1) throw await authFailed(event, realm);
const username = authDecoded.slice(0, colonIndex);
const password = authDecoded.slice(colonIndex + 1);
if (!opts.validate && (!username || !password)) throw await authFailed(event, realm);
const usernameOk = !opts.username || timingSafeEqual(username, opts.username);
const passwordOk = !opts.password || timingSafeEqual(password, opts.password);
const validateOk = !opts.validate || await opts.validate(username, password);
if (!usernameOk || !passwordOk || !validateOk) throw await authFailed(event, realm);
const context = getEventContext(event);
context.basicAuth = {
username,
password,
realm
};
return true;
}
function basicAuth(opts) {
return async (event, next) => {
await requireBasicAuth(event, opts);
return next();
};
}
async function authFailed(event, realm) {
await randomJitter();
return new HTTPError({
status: 401,
statusText: "Authentication required",
headers: { "www-authenticate": `Basic realm="${quoteRealm(realm)}", charset="UTF-8"` }
});
}
function quoteRealm(realm) {
return realm.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "").replace(/["\\]/g, "\\$&");
}
async function getRequestFingerprint(event, opts = {}) {
const fingerprint = [];
let hasValue = false;
const addComponent = (value) => {
if (value) hasValue = true;
fingerprint.push(value ? escapeComponent(value) : "");
};
if (opts.ip !== false) addComponent(getRequestIP(event, { xForwardedFor: opts.xForwardedFor }));
if (opts.method === true) addComponent(event.req.method);
if (opts.url === true) addComponent(event.req.url);
if (opts.userAgent === true) addComponent(event.req.headers.get("user-agent"));
if (!hasValue) return null;
const fingerprintString = fingerprint.join("|");
if (opts.hash === false) return fingerprintString;
const buffer = await crypto.subtle.digest(opts.hash || "SHA-256", new TextEncoder().encode(fingerprintString));
return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
function escapeComponent(value) {
return value.replaceAll("%", "%25").replaceAll("|", "%7C");
}
function defineWebSocket(hooks) {
return hooks;
}
function defineWebSocketHandler(hooks, http) {
return defineHandler(function _webSocketHandler(event) {
if (http && !isWebSocketUpgrade(event)) return http(event);
const crossws = typeof hooks === "function" ? hooks(event) : hooks;
if (crossws instanceof Promise) return crossws.then((resolved) => toUpgradeResponse(event, resolved));
return toUpgradeResponse(event, crossws);
});
}
function isWebSocketUpgrade(event) {
return event.req.headers.get("upgrade")?.toLowerCase() === "websocket";
}
const kWebSocketHooks = /* @__PURE__ */ Symbol.for("crossws.hooks");
function toUpgradeResponse(event, crossws) {
try {
const req = event.req;
if (req.context) req.context[kWebSocketHooks] = crossws;
req[kWebSocketHooks] = crossws;
} catch {}
return Object.assign(new Response("WebSocket upgrade is required.", { status: 426 }), { crossws });
}
const PARSE_ERROR = -32700;
const INVALID_REQUEST = -32600;
const METHOD_NOT_FOUND = -32601;
const INVALID_PARAMS = -32602;
const DEFAULT_MAX_BATCH_SIZE = 50;
function defineJsonRpcHandler(opts = {}) {
const methodMap = createMethodMap(opts.methods);
const maxBatchSize = opts.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
const handler = async (event) => {
if (event.req.method !== "POST") throw new HTTPError({ status: 405 });
if (opts.validateContentType !== false && !isJsonContentType(event.req.headers.get("content-type"))) throw new HTTPError({
status: 415,
message: "Unsupported Media Type"
});
assertAllowedOrigin(event, opts.allowedOrigins);
let body;
try {
body = await event.req.json();
} catch (error) {
if (HTTPError.isError(error)) throw error;
return createJsonRpcError(null, PARSE_ERROR, "Parse error");
}
const result = await processJsonRpcBody(body, methodMap, event, maxBatchSize);
return result === void 0 ? new HTTPResponse("", { status: 202 }) : result;
};
return defineHandler({
...opts,
handler
});
}
function isJsonContentType(value) {
if (!value) return false;
const mediaType = value.split(";")[0].trim().toLowerCase();
return mediaType === "application/json" || mediaType === "application/json-rpc" || mediaType.endsWith("+json");
}
function assertAllowedOrigin(event, allowedOrigins) {
const origin = event.req.headers.get("origin");
if (!origin || allowedOrigins === "*") return;
if (!(allowedOrigins ? isCorsOriginAllowed(origin, { origin: typeof allowedOrigins === "string" ? [allowedOrigins] : allowedOrigins }) : origin === event.url.origin)) throw new HTTPError({
status: 403,
message: "Origin not allowed"
});
}
function defineJsonRpcWebSocketHandler(opts) {
const methodMap = createMethodMap(opts.methods);
const maxBatchSize = opts.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
return defineWebSocketHandler({
...opts.hooks,
async message(peer, message) {
let body;
try {
body = message.json();
} catch {
peer.send(JSON.stringify(createJsonRpcError(null, PARSE_ERROR, "Parse error")));
return;
}
const result = await processJsonRpcBody(body, methodMap, peer, maxBatchSize);
if (result !== void 0) peer.send(JSON.stringify(result));
}
});
}
function createMethodMap(methods) {
const methodMap = Object.create(null);
for (const key of Object.keys(methods)) methodMap[key] = methods[key];
return methodMap;
}
async function processJsonRpcBody(body, methodMap, context, maxBatchSize) {
if (!body || typeof body !== "object") return createJsonRpcError(null, INVALID_REQUEST, "Invalid Request");
const requests = Array.isArray(body) ? body : [body];
if (requests.length === 0) return createJsonRpcError(null, INVALID_REQUEST, "Invalid Request");
if (requests.length > maxBatchSize) return createJsonRpcError(null, INVALID_REQUEST, `Invalid Request: batch size exceeds maximum of ${maxBatchSize}`);
const finalResponses = (await Promise.all(requests.map((raw) => processJsonRpcMethod(raw, methodMap, context)))).filter((r) => r !== void 0);
if (finalResponses.length === 0) return;
return Array.isArray(body) ? finalResponses : finalResponses[0];
}
async function processJsonRpcMethod(raw, methodMap, context) {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return createJsonRpcError(null, INVALID_REQUEST, "Invalid Request");
const req = raw;
if (req.jsonrpc !== "2.0" || typeof req.method !== "string" || "id" in req && !isValidId(req.id)) {
const id = "id" in req && isValidId(req.id) ? req.id : null;
return createJsonRpcError(id, INVALID_REQUEST, "Invalid Request");
}
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");
if (req.method.startsWith("rpc.")) return isNotification(req) ? void 0 : createJsonRpcError(req.id, METHOD_NOT_FOUND, "Method not found");
const method = req.method;
const params = req.params;
const notification = isNotification(req);
const id = notification ? void 0 : req.id;
const methodHandler = methodMap[method];
if (!methodHandler) return notification ? void 0 : createJsonRpcError(id, METHOD_NOT_FOUND, "Method not found");
try {
const rpcReq = {
jsonrpc: "2.0",
method,
params
};
if (!notification) rpcReq.id = id;
const result = await methodHandler(rpcReq, context);
return notification ? void 0 : {
jsonrpc: "2.0",
id,
result: result ?? null
};
} catch (error_) {
if (notification) return;
const h3Error = HTTPError.isError(error_) && !error_.unhandled ? error_ : {
status: HTTPError.isError(error_) ? error_.status : 500,
message: "Internal error",
data: void 0
};
const statusCode = h3Error.status;
const statusMessage = h3Error.message;
const errorCode = mapHttpStatusToJsonRpcError(statusCode);
return createJsonRpcError(id, errorCode, statusMessage, h3Error.data);
}
}
function mapHttpStatusToJsonRpcError(status) {
switch (status) {
case 400:
case 422: return INVALID_PARAMS;
case 401: return -32001;
case 403: return -32003;
case 404: return -32004;
case 408: return -32008;
case 409: return -32009;
case 429: return -32029;
default:
if (status >= 300 && status < 500) return -32e3;
return -32603;
}
}
function isNotification(req) {
return !("id" in req);
}
function isValidId(id) {
if (id === null) return true;
if (typeof id === "string") return true;
return typeof id === "number" && Number.isInteger(id);
}
const createJsonRpcError = (id, code, message, data) => {
const error = {
code,
message
};
if (data !== void 0) error.data = data;
return {
jsonrpc: "2.0",
id,
error
};
};
const H3Error = HTTPError;
function createError(arg1, arg2) {
return new HTTPError(arg1, arg2);
}
function isError(input) {
return HTTPError.isError(input);
}
const getRequestPath = (event) => event.path;
function getRequestHeader(event, name) {
return event.req.headers.get(name) || void 0;
}
const getHeader = getRequestHeader;
function getRequestHeaders(event) {
return Object.fromEntries(event.req.headers.entries());
}
const getHeaders = getRequestHeaders;
function getMethod(event, defaultMethod = "GET") {
return (event.req.method || defaultMethod).toUpperCase();
}
function readRawBody(event, encoding = "utf8") {
return encoding ? event.req.text() : event.req.arrayBuffer().then((r) => new Uint8Array(r));
}
async function readFormDataBody(event) {
return event.req.formData();
}
const readFormData = readFormDataBody;
async function readMultipartFormData(event) {
const formData = await event.req.formData();
return Promise.all([...formData.entries()].map(async ([key, value]) => {
return typeof value === "object" ? {
name: key,
type: value.type,
filename: value.name,
data: await value.bytes()
} : {
name: key,
data: new TextEncoder().encode(value)
};
}));
}
function getBodyStream(event) {
return event.req.body || void 0;
}
const getRequestWebStream = getBodyStream;
function sendStream(_event, value) {
return value;
}
const sendNoContent = (_, code) => noContent(code);
const sendRedirect = (_, loc, code) => redirect(loc, code);
const sendWebResponse = (response) => response;
const sendProxy = proxy;
function createEventStream(event, opts) {
return new EventStream(event, opts);
}
const sendIterable = (_event, val, options) => {
return iterable(val, options);
};
function getResponseStatusText(event) {
return event.res.statusText || "";
}
function appendResponseHeader(event, name, value) {
if (Array.isArray(value)) for (const valueItem of value) event.res.headers.append(name, valueItem);
else event.res.headers.append(name, value);
}
const appendHeader = appendResponseHeader;
function setResponseHeader(event, name, value) {
if (Array.isArray(value)) {
event.res.headers.delete(name);
for (const valueItem of value) event.res.headers.append(name, valueItem);
} else event.res.headers.set(name, value);
}
const setHeader = setResponseHeader;
function setResponseHeaders(event, headers) {
for (const [name, value] of Object.entries(headers)) event.res.headers.set(name, value);
}
const setHeaders = setResponseHeaders;
function getResponseStatus(event) {
return event.res.status || 200;
}
function setResponseStatus(event, code, text) {
if (code) event.res.status = sanitizeStatusCode(code, event.res.status);
if (text) event.res.statusText = sanitizeStatusMessage(text);
}
function defaultContentType(event, type) {
if (type && event.res.status !== 304 && !event.res.headers.has("content-type")) event.res.headers.set("content-type", type);
}
function getResponseHeaders(event) {
return Object.fromEntries(event.res.headers.entries());
}
function getResponseHeader(event, name) {
return event.res.headers.get(name) || void 0;
}
function removeResponseHeader(event, name) {
return event.res.headers.delete(name);
}
function appendResponseHeaders(event, headers) {
for (const [name, value] of Object.entries(headers)) appendResponseHeader(event, name, value);
}
const appendHeaders = appendResponseHeaders;
function clearResponseHeaders(event, headerNames) {
if (headerNames && headerNames.length > 0) for (const name of headerNames) event.res.headers.delete(name);
else for (const name of event.res.headers.keys()) event.res.headers.delete(name);
}
const defineEventHandler = defineHandler;
const eventHandler = defineHandler;
const lazyEventHandler = defineLazyEventHandler;
const defineNodeListener = defineNodeHandler;
const fromNodeMiddleware = fromNodeHandler;
function toNodeHandler(app) {
if (toNodeHandler._isWarned !== true) {
console.warn(`[h3] "toNodeHandler" export from h3 is deprecated. Please import "toNodeHandler" from "h3/node".`);
toNodeHandler._isWarned = true;
}
return (toNodeHandler._toNodeHandler ??= () => {
return globalThis.process.getBuiltinModule("node:module").createRequire(import.meta.url)("srvx/node").toNodeHandler;
})()(app.fetch);
}
const toNodeListener = toNodeHandler;
const createApp = (config) => new H3(config);
const createRouter$1 = (config) => new H3(config);
const useBase = withBase;
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 };