UNPKG

elysia

Version:

Ergonomic Framework for Human

702 lines (694 loc) 22.1 kB
// src/cookies.ts import { parse, serialize } from "cookie"; import decode from "fast-decode-uri-component"; // src/universal/utils.ts var isBun = typeof Bun < "u"; // src/utils.ts var hasHeaderShorthand = "toJSON" in new Headers(); var primitiveHooks = [ "start", "request", "parse", "transform", "resolve", "beforeHandle", "afterHandle", "mapResponse", "afterResponse", "trace", "error", "stop", "body", "headers", "params", "query", "response", "type", "detail" ], primitiveHookMap = primitiveHooks.reduce( (acc, x) => (acc[x] = !0, acc), {} ); var isBun2 = typeof Bun < "u", hasBunHash = isBun2 && typeof Bun.hash == "function"; var StatusMap = { Continue: 100, "Switching Protocols": 101, Processing: 102, "Early Hints": 103, OK: 200, Created: 201, Accepted: 202, "Non-Authoritative Information": 203, "No Content": 204, "Reset Content": 205, "Partial Content": 206, "Multi-Status": 207, "Already Reported": 208, "Multiple Choices": 300, "Moved Permanently": 301, Found: 302, "See Other": 303, "Not Modified": 304, "Temporary Redirect": 307, "Permanent Redirect": 308, "Bad Request": 400, Unauthorized: 401, "Payment Required": 402, Forbidden: 403, "Not Found": 404, "Method Not Allowed": 405, "Not Acceptable": 406, "Proxy Authentication Required": 407, "Request Timeout": 408, Conflict: 409, Gone: 410, "Length Required": 411, "Precondition Failed": 412, "Payload Too Large": 413, "URI Too Long": 414, "Unsupported Media Type": 415, "Range Not Satisfiable": 416, "Expectation Failed": 417, "I'm a teapot": 418, "Misdirected Request": 421, "Unprocessable Content": 422, Locked: 423, "Failed Dependency": 424, "Too Early": 425, "Upgrade Required": 426, "Precondition Required": 428, "Too Many Requests": 429, "Request Header Fields Too Large": 431, "Unavailable For Legal Reasons": 451, "Internal Server Error": 500, "Not Implemented": 501, "Bad Gateway": 502, "Service Unavailable": 503, "Gateway Timeout": 504, "HTTP Version Not Supported": 505, "Variant Also Negotiates": 506, "Insufficient Storage": 507, "Loop Detected": 508, "Not Extended": 510, "Network Authentication Required": 511 }, InvertedStatusMap = Object.fromEntries( Object.entries(StatusMap).map(([k, v]) => [v, k]) ); var encoder = new TextEncoder(); var ELYSIA_FORM_DATA = Symbol("ElysiaFormData"), ELYSIA_REQUEST_ID = Symbol("ElysiaRequestId"); var isNotEmpty = (obj) => { if (!obj) return !1; for (let _ in obj) return !0; return !1; }; var supportPerMethodInlineHandler = (() => { if (typeof Bun > "u") return !0; let semver = Bun.version.split("."); return !(+semver[0] < 1 || +semver[1] < 2 || +semver[2] < 14); })(); // src/error.ts import { Value } from "@sinclair/typebox/value"; var env = typeof Bun < "u" ? Bun.env : typeof process < "u" ? process?.env : void 0, ERROR_CODE = Symbol("ElysiaErrorCode"), isProduction = (env?.NODE_ENV ?? env?.ENV) === "production", emptyHttpStatus = { 101: void 0, 204: void 0, 205: void 0, 304: void 0, 307: void 0, 308: void 0 }, ElysiaCustomStatusResponse = class { constructor(code, response) { let res = response ?? (code in InvertedStatusMap ? ( // @ts-expect-error Always correct InvertedStatusMap[code] ) : code); this.code = StatusMap[code] ?? code, code in emptyHttpStatus ? this.response = void 0 : this.response = res; } }; // src/cookies.ts var Cookie = class { constructor(name, jar, initial = {}) { this.name = name; this.jar = jar; this.initial = initial; } get cookie() { return this.jar[this.name] ?? this.initial; } set cookie(jar) { this.name in this.jar || (this.jar[this.name] = this.initial), this.jar[this.name] = jar; } get setCookie() { return this.name in this.jar || (this.jar[this.name] = this.initial), this.jar[this.name]; } set setCookie(jar) { this.cookie = jar; } get value() { return this.cookie.value; } set value(value) { this.setCookie.value = value; } get expires() { return this.cookie.expires; } set expires(expires) { this.setCookie.expires = expires; } get maxAge() { return this.cookie.maxAge; } set maxAge(maxAge) { this.setCookie.maxAge = maxAge; } get domain() { return this.cookie.domain; } set domain(domain) { this.setCookie.domain = domain; } get path() { return this.cookie.path; } set path(path) { this.setCookie.path = path; } get secure() { return this.cookie.secure; } set secure(secure) { this.setCookie.secure = secure; } get httpOnly() { return this.cookie.httpOnly; } set httpOnly(httpOnly) { this.setCookie.httpOnly = httpOnly; } get sameSite() { return this.cookie.sameSite; } set sameSite(sameSite) { this.setCookie.sameSite = sameSite; } get priority() { return this.cookie.priority; } set priority(priority) { this.setCookie.priority = priority; } get partitioned() { return this.cookie.partitioned; } set partitioned(partitioned) { this.setCookie.partitioned = partitioned; } get secrets() { return this.cookie.secrets; } set secrets(secrets) { this.setCookie.secrets = secrets; } update(config) { return this.setCookie = Object.assign( this.cookie, typeof config == "function" ? config(this.cookie) : config ), this; } set(config) { return this.setCookie = Object.assign( { ...this.initial, value: this.value }, typeof config == "function" ? config(this.cookie) : config ), this; } remove() { if (this.value !== void 0) return this.set({ expires: /* @__PURE__ */ new Date(0), maxAge: 0, value: "" }), this; } toString() { return typeof this.value == "object" ? JSON.stringify(this.value) : this.value?.toString() ?? ""; } }; var serializeCookie = (cookies) => { if (!cookies || !isNotEmpty(cookies)) return; let set2 = []; for (let [key, property] of Object.entries(cookies)) { if (!key || !property) continue; let value = property.value; value != null && set2.push( serialize( key, typeof value == "object" ? JSON.stringify(value) : value + "", property ) ); } if (set2.length !== 0) return set2.length === 1 ? set2[0] : set2; }; // src/adapter/utils.ts var handleFile = (response, set2) => { if (!isBun && response instanceof Promise) return response.then((res) => handleFile(res, set2)); let size = response.size, immutable = set2 && (set2.status === 206 || set2.status === 304 || set2.status === 412 || set2.status === 416), defaultHeader = immutable ? { "transfer-encoding": "chunked" } : { "accept-ranges": "bytes", "content-range": size ? `bytes 0-${size - 1}/${size}` : void 0, "transfer-encoding": "chunked" }; if (!set2 && !size) return new Response(response); if (!set2) return new Response(response, { headers: defaultHeader }); if (set2.headers instanceof Headers) { let setHeaders = defaultHeader; if (hasHeaderShorthand) setHeaders = set2.headers.toJSON(); else { setHeaders = {}; for (let [key, value] of set2.headers.entries()) key in set2.headers && (setHeaders[key] = value); } return immutable && (delete set2.headers["content-length"], delete set2.headers["accept-ranges"]), new Response(response, { status: set2.status, headers: setHeaders }); } return isNotEmpty(set2.headers) ? new Response(response, { status: set2.status, headers: Object.assign(defaultHeader, set2.headers) }) : new Response(response, { status: set2.status, headers: defaultHeader }); }, parseSetCookies = (headers, setCookie) => { if (!headers) return headers; headers.delete("set-cookie"); for (let i = 0; i < setCookie.length; i++) { let index = setCookie[i].indexOf("="); headers.append( "set-cookie", `${setCookie[i].slice(0, index)}=${setCookie[i].slice(index + 1) || ""}` ); } return headers; }, responseToSetHeaders = (response, set2) => { if (set2?.headers) { if (response) if (hasHeaderShorthand) Object.assign(set2.headers, response.headers.toJSON()); else for (let [key, value] of response.headers.entries()) key in set2.headers && (set2.headers[key] = value); return set2.status === 200 && (set2.status = response.status), set2.headers["content-encoding"] && delete set2.headers["content-encoding"], set2; } if (!response) return { headers: {}, status: set2?.status ?? 200 }; if (hasHeaderShorthand) return set2 = { headers: response.headers.toJSON(), status: set2?.status ?? 200 }, set2.headers["content-encoding"] && delete set2.headers["content-encoding"], set2; set2 = { headers: {}, status: set2?.status ?? 200 }; for (let [key, value] of response.headers.entries()) key !== "content-encoding" && key in set2.headers && (set2.headers[key] = value); return set2; }, createStreamHandler = ({ mapResponse: mapResponse2, mapCompactResponse: mapCompactResponse2 }) => async (generator, set2, request) => { let init = generator.next(); if (init instanceof Promise && (init = await init), typeof init?.done > "u" || init?.done) return set2 ? mapResponse2(init.value, set2, request) : mapCompactResponse2(init.value, request); let contentType = ( // @ts-ignore init.value && typeof init.value?.stream ? "text/event-stream" : init.value && typeof init.value == "object" ? "application/json" : "text/plain" ); return set2?.headers ? (set2.headers["transfer-encoding"] || (set2.headers["transfer-encoding"] = "chunked"), set2.headers["content-type"] || (set2.headers["content-type"] = contentType), set2.headers["cache-control"] || (set2.headers["cache-control"] = "no-cache")) : set2 = { status: 200, headers: { "content-type": contentType, "transfer-encoding": "chunked", "cache-control": "no-cache", connection: "keep-alive" } }, new Response( new ReadableStream({ async start(controller) { let end = !1; if (request?.signal?.addEventListener("abort", () => { end = !0; try { controller.close(); } catch { } }), init.value !== void 0 && init.value !== null) if (init.value.toStream) controller.enqueue(init.value.toStream()); else if (typeof init.value == "object") try { controller.enqueue( Buffer.from(JSON.stringify(init.value)) ); } catch { controller.enqueue( Buffer.from(init.value.toString()) ); } else controller.enqueue( Buffer.from(init.value.toString()) ); for await (let chunk of generator) { if (end) break; if (chunk != null) { if (chunk.toStream) controller.enqueue(chunk.toStream()); else if (typeof chunk == "object") try { controller.enqueue( Buffer.from(JSON.stringify(chunk)) ); } catch { controller.enqueue( Buffer.from(chunk.toString()) ); } else controller.enqueue(Buffer.from(chunk.toString())); await new Promise( (resolve) => setTimeout(() => resolve(), 0) ); } } try { controller.close(); } catch { } } }), set2 ); }; async function* streamResponse(response) { let body = response.body; if (!body) return; let reader = body.getReader(), decoder = new TextDecoder(); try { for (; ; ) { let { done, value } = await reader.read(); if (done) break; yield decoder.decode(value); } } finally { reader.releaseLock(); } } var handleSet = (set2) => { if (typeof set2.status == "string" && (set2.status = StatusMap[set2.status]), set2.cookie && isNotEmpty(set2.cookie)) { let cookie = serializeCookie(set2.cookie); cookie && (set2.headers["set-cookie"] = cookie); } set2.headers["set-cookie"] && Array.isArray(set2.headers["set-cookie"]) && (set2.headers = parseSetCookies( new Headers(set2.headers), set2.headers["set-cookie"] )); }, createResponseHandler = (handler) => { let handleStream2 = createStreamHandler(handler); return (response, set2, request) => { let isCookieSet = !1; if (set2.headers instanceof Headers) for (let key of set2.headers.keys()) if (key === "set-cookie") { if (isCookieSet) continue; isCookieSet = !0; for (let cookie of set2.headers.getSetCookie()) response.headers.append("set-cookie", cookie); } else response.headers.append(key, set2.headers?.get(key) ?? ""); else for (let key in set2.headers) response.headers.append( key, set2.headers[key] ); let status = set2.status ?? 200; return response.status !== status && status !== 200 && (response.status <= 300 || response.status > 400) ? response.text().then((value) => { let newResponse = new Response(value, { headers: response.headers, status: set2.status }); return !newResponse.headers.has("content-length") && newResponse.headers.get( "transfer-encoding" ) === "chunked" ? handleStream2( streamResponse(newResponse), responseToSetHeaders(newResponse, set2), request ) : newResponse; }) : !response.headers.has("content-length") && response.headers.get("transfer-encoding") === "chunked" ? handleStream2( streamResponse(response), responseToSetHeaders(response, set2), request ) : response; }; }; // src/adapter/bun/handler.ts var mapResponse = (response, set2, request) => { if (isNotEmpty(set2.headers) || set2.status !== 200 || set2.cookie) switch (handleSet(set2), response?.constructor?.name) { case "String": return new Response(response, set2); case "Array": case "Object": return set2.headers["content-type"] = "application/json", new Response(JSON.stringify(response), set2); case "ElysiaFile": return handleFile(response.value); case "File": return handleFile(response, set2); case "Blob": return handleFile(response, set2); case "ElysiaCustomStatusResponse": return set2.status = response.code, mapResponse( response.response, set2, request ); case "ReadableStream": return set2.headers["content-type"]?.startsWith( "text/event-stream" ) || (set2.headers["content-type"] = "text/event-stream; charset=utf-8"), request?.signal?.addEventListener( "abort", { handleEvent() { request?.signal && !request?.signal?.aborted && response.cancel(); } }, { once: !0 } ), new Response(response, set2); case void 0: return response ? new Response(JSON.stringify(response), set2) : new Response("", set2); case "Response": return handleResponse(response, set2, request); case "Error": return errorToResponse(response, set2); case "Promise": return response.then( (x) => mapResponse(x, set2, request) ); case "Function": return mapResponse(response(), set2, request); case "Number": case "Boolean": return new Response( response.toString(), set2 ); case "Cookie": return response instanceof Cookie ? new Response(response.value, set2) : new Response(response?.toString(), set2); case "FormData": return new Response(response, set2); default: if (response instanceof Response) return handleResponse(response, set2, request); if (response instanceof Promise) return response.then((x) => mapResponse(x, set2)); if (response instanceof Error) return errorToResponse(response, set2); if (response instanceof ElysiaCustomStatusResponse) return set2.status = response.code, mapResponse( response.response, set2, request ); if (typeof response?.next == "function") return handleStream(response, set2, request); if (typeof response?.then == "function") return response.then((x) => mapResponse(x, set2)); if (typeof response?.toResponse == "function") return mapResponse(response.toResponse(), set2); if ("charCodeAt" in response) { let code = response.charCodeAt(0); if (code === 123 || code === 91) return set2.headers["Content-Type"] || (set2.headers["Content-Type"] = "application/json"), new Response( JSON.stringify(response), set2 ); } return new Response(response, set2); } return response instanceof Response && !response.headers.has("content-length") && response.headers.get("transfer-encoding") === "chunked" ? handleStream( streamResponse(response), responseToSetHeaders(response, set2), request ) : ( // @ts-expect-error typeof response?.next == "function" || response instanceof ReadableStream ? handleStream(response, set2, request) : mapCompactResponse(response, request) ); }; var mapCompactResponse = (response, request) => { switch (response?.constructor?.name) { case "String": return new Response(response); case "Object": case "Array": return new Response(JSON.stringify(response), { headers: { "Content-Type": "application/json" } }); case "ElysiaFile": return handleFile(response.value); case "File": return handleFile(response); case "Blob": return handleFile(response); case "ElysiaCustomStatusResponse": return mapResponse( response.response, { status: response.code, headers: {} } ); case "ReadableStream": return request?.signal?.addEventListener( "abort", { handleEvent() { request?.signal && !request?.signal?.aborted && response.cancel(); } }, { once: !0 } ), new Response(response, { headers: { "Content-Type": "text/event-stream; charset=utf-8" } }); case void 0: return response ? new Response(JSON.stringify(response), { headers: { "content-type": "application/json" } }) : new Response(""); case "Response": return response.headers.get("transfer-encoding") === "chunked" ? handleStream( streamResponse(response), responseToSetHeaders(response), request ) : response; case "Error": return errorToResponse(response); case "Promise": return response.then( (x) => mapCompactResponse(x, request) ); // ? Maybe response or Blob case "Function": return mapCompactResponse(response(), request); case "Number": case "Boolean": return new Response(response.toString()); case "FormData": return new Response(response); default: if (response instanceof Response) return response; if (response instanceof Promise) return response.then( (x) => mapCompactResponse(x, request) ); if (response instanceof Error) return errorToResponse(response); if (response instanceof ElysiaCustomStatusResponse) return mapResponse( response.response, { status: response.code, headers: {} } ); if (typeof response?.next == "function") return handleStream(response, void 0, request); if (typeof response?.then == "function") return response.then((x) => mapResponse(x, set)); if (typeof response?.toResponse == "function") return mapCompactResponse(response.toResponse()); if ("charCodeAt" in response) { let code = response.charCodeAt(0); if (code === 123 || code === 91) return new Response(JSON.stringify(response), { headers: { "Content-Type": "application/json" } }); } return new Response(response); } }, errorToResponse = (error, set2) => new Response( JSON.stringify({ name: error?.name, message: error?.message, cause: error?.cause }), { status: set2?.status !== 200 ? set2?.status ?? 500 : 500, headers: set2?.headers } ); var handleResponse = createResponseHandler({ mapResponse, mapCompactResponse }), handleStream = createStreamHandler({ mapResponse, mapCompactResponse }); // src/adapter/bun/handler-native.ts var createNativeStaticHandler = (handle, hooks, setHeaders = {}) => { if (typeof handle == "function" || handle instanceof Blob) return; if (typeof handle == "object" && handle?.toString() === "[object HTMLBundle]") return () => handle; let response = mapResponse(handle, { headers: setHeaders }); if (!hooks.parse?.length && !hooks.transform?.length && !hooks.beforeHandle?.length && !hooks.afterHandle?.length) return response instanceof Promise ? response.then((response2) => { if (response2) return response2.headers.has("content-type") || response2.headers.append("content-type", "text/plain"), response2.clone(); }) : (response.headers.has("content-type") || response.headers.append("content-type", "text/plain"), () => response.clone()); }; export { createNativeStaticHandler };