h3
Version:
Minimal H(TTP) framework built for high performance and portability.
293 lines (292 loc) • 11.3 kB
JavaScript
import { HTTPResponse, onDispose as onDispose$1 } from "./response.mjs";
const textEncoder = /* @__PURE__ */ new TextEncoder();
const textDecoder = /* @__PURE__ */ new TextDecoder();
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
function base64Encode(data) {
const buff = validateBinaryLike(data);
if (globalThis.Buffer) return globalThis.Buffer.from(buff).toString("base64url");
let result = "";
let i;
const len = buff.length;
for (i = 2; i < len; i += 3) result += base64Chars[buff[i - 2] >> 2] + base64Chars[(buff[i - 2] & 3) << 4 | buff[i - 1] >> 4] + base64Chars[(buff[i - 1] & 15) << 2 | buff[i] >> 6] + base64Chars[buff[i] & 63];
if (i === len + 1) result += base64Chars[buff[i - 2] >> 2] + base64Chars[(buff[i - 2] & 3) << 4];
if (i === len) result += base64Chars[buff[i - 2] >> 2] + base64Chars[(buff[i - 2] & 3) << 4 | buff[i - 1] >> 4] + base64Chars[(buff[i - 1] & 15) << 2];
return result;
}
function base64Decode(b64Url) {
if (globalThis.Buffer) return new Uint8Array(globalThis.Buffer.from(b64Url, "base64url"));
const b64 = b64Url.replace(/-/g, "+").replace(/_/g, "/");
const binString = atob(b64);
const size = binString.length;
const bytes = new Uint8Array(size);
for (let i = 0; i < size; i++) bytes[i] = binString.charCodeAt(i);
return bytes;
}
function validateBinaryLike(source) {
if (typeof source === "string") return textEncoder.encode(source);
else if (source instanceof Uint8Array) return source;
else if (source instanceof ArrayBuffer) return new Uint8Array(source);
throw new TypeError(`The input must be a Uint8Array, a string, or an ArrayBuffer.`);
}
function serializeIterableValue(value) {
switch (typeof value) {
case "string": return textEncoder.encode(value);
case "boolean":
case "number":
case "bigint":
case "symbol": return textEncoder.encode(value.toString());
case "object":
if (value instanceof Uint8Array) return value;
return textEncoder.encode(JSON.stringify(value));
}
return /* @__PURE__ */ new Uint8Array();
}
function coerceIterable(iterable) {
if (typeof iterable === "function") iterable = iterable();
if (Symbol.iterator in iterable) return iterable[Symbol.iterator]();
if (Symbol.asyncIterator in iterable) return iterable[Symbol.asyncIterator]();
return iterable;
}
function onDispose(event, cb) {
onDispose$1(event, cb);
}
function noContent(status = 204) {
return new HTTPResponse(null, {
status,
statusText: "No Content"
});
}
function redirect(location, status = 302, statusText) {
const body = `<html><head><meta http-equiv="refresh" content="0; url=${escapeHtml(location)}" /></head></html>`;
return new HTTPResponse(body, {
status,
statusText: statusText || (status === 301 ? "Moved Permanently" : "Found"),
headers: {
"content-type": "text/html; charset=utf-8",
location
}
});
}
function redirectBack(event, opts = {}) {
const referer = event.req.headers.get("referer");
let location = opts.fallback ?? "/";
if (referer && URL.canParse(referer)) {
const refererURL = new URL(referer);
if (refererURL.origin === event.url.origin) {
let pathname = refererURL.pathname;
if (pathname.startsWith("//")) pathname = "/" + pathname.replace(/^\/+/, "");
location = pathname + (opts.allowQuery ? refererURL.search : "");
}
}
return redirect(location, opts.status);
}
function writeEarlyHints(event, hints) {
const linkValues = [];
for (const [name, value] of Object.entries(hints)) if (name.toLowerCase() === "link") {
for (const v of Array.isArray(value) ? value : [value]) if (v) linkValues.push(v);
}
if (event.runtime?.node?.res?.writeEarlyHints) {
if (linkValues.length === 0) return Promise.resolve();
const normalizedHints = { link: linkValues };
for (const [name, value] of Object.entries(hints)) if (name.toLowerCase() !== "link") normalizedHints[name] = value;
return new Promise((resolve) => {
event.runtime?.node?.res?.writeEarlyHints(normalizedHints, () => resolve());
});
}
for (const v of linkValues) event.res.headers.append("link", v);
}
async function iterable(iterable, options) {
const serializer = options?.serializer ?? serializeIterableValue;
const iterator = coerceIterable(iterable);
let first = await iterator.next();
return new HTTPResponse(new ReadableStream({
async pull(controller) {
const { value, done } = first ?? await iterator.next();
first = void 0;
if (value !== void 0) {
const chunk = serializer(value);
if (chunk !== void 0) controller.enqueue(chunk);
}
if (done) controller.close();
},
cancel() {
iterator.return?.();
}
}));
}
function html(first, ...values) {
let body;
if (typeof first === "string") {
body = escapeHtml(first);
if (body !== first && html._isWarned !== true) {
html._isWarned = true;
console.warn("[h3] `html()` received a plain string containing HTML characters and escaped it. Use the html`` tagged template for dynamic values, or wrap trusted markup with `raw()`.");
}
} else if (isRawHTML(first)) body = first.value;
else body = first.reduce((out, str, i) => {
const value = values[i];
const rendered = value == null ? "" : isRawHTML(value) ? value.value : escapeHtml(String(value));
return out + str + rendered;
}, "");
return new HTTPResponse(body, { headers: { "content-type": "text/html; charset=utf-8" } });
}
function raw(value) {
return {
[kRawHTML]: true,
value
};
}
const kRawHTML = /* @__PURE__ */ Symbol("h3.rawHTML");
function isRawHTML(value) {
return typeof value === "object" && value !== null && value[kRawHTML] === true;
}
const HTML_ESCAPES = {
"&": "&",
"\"": """,
"'": "'",
"<": "<",
">": ">"
};
function escapeHtml(str) {
return str.replace(/[&"'<>]/g, (c) => HTML_ESCAPES[c]);
}
function resolveCorsOptions(options = {}) {
const defaultOptions = {
origin: "*",
methods: "*",
allowHeaders: "*",
exposeHeaders: "*",
credentials: false,
maxAge: false,
preflight: { statusCode: 204 }
};
const resolved = {
...defaultOptions,
...options,
preflight: {
...defaultOptions.preflight,
...options.preflight
}
};
if (resolved.credentials && resolved.origin === "*") warnOnce("[h3] CORS: `credentials: true` with wildcard origin is not allowed. Browsers will reject the response.");
if (resolved.credentials && (resolved.origin === "null" || Array.isArray(resolved.origin) && resolved.origin.includes("null"))) warnOnce("[h3] CORS: `credentials: true` with a `\"null\"` origin is dangerous. Any sandboxed iframe, `data:`/`file:` document, or opaque origin sends `Origin: null`, so credentials would be shared across untrusted contexts.");
if (resolved.credentials && resolved.exposeHeaders === "*") warnOnce("[h3] CORS: `credentials: true` with wildcard `exposeHeaders` has no effect. Browsers treat `*` literally on credentialed requests — list the headers explicitly.");
return resolved;
}
function isCorsOriginAllowed(origin, options) {
const { origin: originOption } = options;
if (!origin) return false;
if (!originOption || originOption === "*") return true;
if (typeof originOption === "function") return originOption(origin);
if (Array.isArray(originOption)) return originOption.some((_origin) => {
if (_origin instanceof RegExp) return _origin.test(origin);
return origin === _origin;
});
return originOption === origin;
}
function createOriginHeaders(event, options) {
const { origin: originOption } = options;
const origin = event.req.headers.get("origin");
if (!originOption || originOption === "*") return { "access-control-allow-origin": "*" };
if (isCorsOriginAllowed(origin, options)) return {
"access-control-allow-origin": origin,
vary: "origin"
};
return { vary: "origin" };
}
function createMethodsHeaders(event, options) {
const { methods, credentials } = options;
if (!methods) return {};
if (methods === "*") {
if (credentials) {
const requestMethod = event.req.headers.get("access-control-request-method");
return requestMethod ? {
"access-control-allow-methods": requestMethod,
vary: "access-control-request-method"
} : {};
}
return { "access-control-allow-methods": "*" };
}
return methods.length > 0 ? { "access-control-allow-methods": methods.join(",") } : {};
}
function createCredentialsHeaders(options) {
const { credentials } = options;
if (credentials) return { "access-control-allow-credentials": "true" };
return {};
}
function createAllowHeaderHeaders(event, options) {
const { allowHeaders } = options;
if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) {
const header = event.req.headers.get("access-control-request-headers");
return header ? {
"access-control-allow-headers": header,
vary: "access-control-request-headers"
} : { vary: "access-control-request-headers" };
}
return {
"access-control-allow-headers": allowHeaders.join(","),
vary: "access-control-request-headers"
};
}
function createExposeHeaders(options) {
const { exposeHeaders, credentials } = options;
if (!exposeHeaders) return {};
if (exposeHeaders === "*") return credentials ? {} : { "access-control-expose-headers": exposeHeaders };
return { "access-control-expose-headers": exposeHeaders.join(",") };
}
function createMaxAgeHeader(options) {
const { maxAge } = options;
if (maxAge) return { "access-control-max-age": maxAge };
return {};
}
let warnedMessages;
function warnOnce(message) {
warnedMessages ??= /* @__PURE__ */ new Set();
if (warnedMessages.has(message)) return;
warnedMessages.add(message);
console.warn(message);
}
function isPreflightRequest(event) {
const origin = event.req.headers.get("origin");
const accessControlRequestMethod = event.req.headers.get("access-control-request-method");
return event.req.method === "OPTIONS" && !!origin && !!accessControlRequestMethod;
}
function appendCorsPreflightHeaders(event, options) {
const headerGroups = [
createOriginHeaders(event, options),
createCredentialsHeaders(options),
createMethodsHeaders(event, options),
createAllowHeaderHeaders(event, options),
createMaxAgeHeader(options)
];
const headers = Object.assign({}, ...headerGroups);
const varyValues = headerGroups.map((group) => group.vary).filter(Boolean);
if (varyValues.length > 0) headers.vary = varyValues.join(", ");
setCorsHeaders(event, headers);
}
function appendCorsHeaders(event, options) {
setCorsHeaders(event, {
...createOriginHeaders(event, options),
...createCredentialsHeaders(options),
...createExposeHeaders(options)
});
}
function setCorsHeaders(event, headers) {
for (const [key, value] of Object.entries(headers)) if (key === "vary") {
event.res.headers.append(key, value);
event.res.errHeaders.append(key, value);
} else {
event.res.headers.set(key, value);
event.res.errHeaders.set(key, value);
}
}
function handleCors(event, options) {
const _options = resolveCorsOptions(options);
if (isPreflightRequest(event)) {
appendCorsPreflightHeaders(event, _options);
return noContent(_options.preflight.statusCode);
}
appendCorsHeaders(event, _options);
return false;
}
export { appendCorsHeaders, appendCorsPreflightHeaders, base64Decode, base64Encode, handleCors, html, isCorsOriginAllowed, isPreflightRequest, iterable, noContent, onDispose, raw, redirect, redirectBack, textDecoder, textEncoder, writeEarlyHints };