elysia
Version:
Ergonomic Framework for Human
787 lines (780 loc) • 27.9 kB
JavaScript
// src/dynamic-handle.ts
import { TransformDecodeError } from "@sinclair/typebox/value";
// src/error.ts
import { Value } from "@sinclair/typebox/value";
// 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])
);
function removeTrailingEquals(digest) {
let trimmedDigest = digest;
for (; trimmedDigest.endsWith("="); )
trimmedDigest = trimmedDigest.slice(0, -1);
return trimmedDigest;
}
var encoder = new TextEncoder(), signCookie = async (val, secret) => {
if (typeof val != "string")
throw new TypeError("Cookie value must be provided as a string.");
if (secret === null) throw new TypeError("Secret key must be provided.");
let secretKey = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
!1,
["sign"]
), hmacBuffer = await crypto.subtle.sign(
"HMAC",
secretKey,
encoder.encode(val)
);
return val + "." + removeTrailingEquals(Buffer.from(hmacBuffer).toString("base64"));
}, unsignCookie = async (input, secret) => {
if (typeof input != "string")
throw new TypeError("Signed cookie string must be provided.");
if (secret === null) throw new TypeError("Secret key must be provided.");
let tentativeValue = input.slice(0, input.lastIndexOf("."));
return await signCookie(tentativeValue, secret) === input ? tentativeValue : !1;
};
var redirect = (url, status2 = 302) => Response.redirect(url, status2), ELYSIA_FORM_DATA = Symbol("ElysiaFormData"), ELYSIA_REQUEST_ID = Symbol("ElysiaRequestId");
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
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;
}
}, status = (code, response) => new ElysiaCustomStatusResponse(code, response);
var NotFoundError = class extends Error {
constructor(message) {
super(message ?? "NOT_FOUND");
this.code = "NOT_FOUND";
this.status = 404;
}
};
var InvalidCookieSignature = class extends Error {
constructor(key, message) {
super(message ?? `"${key}" has invalid cookie signature`);
this.key = key;
this.code = "INVALID_COOKIE_SIGNATURE";
this.status = 400;
}
}, mapValueError = (error) => {
if (!error)
return {
summary: void 0
};
let { message, path, value, type } = error, property = path.slice(1).replaceAll("/", "."), isRoot = path === "";
switch (type) {
case 42:
return {
...error,
summary: isRoot ? "Value should not be provided" : `Property '${property}' should not be provided`
};
case 45:
return {
...error,
summary: isRoot ? "Value is missing" : `Property '${property}' is missing`
};
case 50:
let quoteIndex = message.indexOf("'"), format = message.slice(
quoteIndex + 1,
message.indexOf("'", quoteIndex + 1)
);
return {
...error,
summary: isRoot ? "Value should be an email" : `Property '${property}' should be ${format}`
};
case 54:
return {
...error,
summary: `${message.slice(0, 9).trim()} property '${property}' to be ${message.slice(8).trim()} but found: ${value}`
};
case 62:
let union = error.schema.anyOf.map((x) => `'${x?.format ?? x.type}'`).join(", ");
return {
...error,
summary: isRoot ? `Value should be one of ${union}` : `Property '${property}' should be one of: ${union}`
};
default:
return { summary: message, ...error };
}
};
var ValidationError = class _ValidationError extends Error {
constructor(type, validator, value, errors) {
value && typeof value == "object" && value instanceof ElysiaCustomStatusResponse && (value = value.response);
let error = errors?.First() || (isProduction ? void 0 : "Errors" in validator ? validator.Errors(value).First() : Value.Errors(validator, value).First()), customError = error?.schema?.message || error?.schema?.error !== void 0 ? typeof error.schema.error == "function" ? error.schema.error({
type,
validator,
value,
get errors() {
return [...validator.Errors(value)].map(
mapValueError
);
}
}) : error.schema.error : void 0, accessor = error?.path || "root", message = "";
if (customError !== void 0)
message = typeof customError == "object" ? JSON.stringify(customError) : customError + "";
else if (isProduction)
message = JSON.stringify({
type: "validation",
on: type,
summary: mapValueError(error).summary,
message: error?.message,
found: value
});
else {
let schema = validator?.schema ?? validator, errors2 = "Errors" in validator ? [...validator.Errors(value)].map(mapValueError) : [...Value.Errors(validator, value)].map(mapValueError), expected;
try {
expected = Value.Create(schema);
} catch (error2) {
expected = {
type: "Could not create expected value",
// @ts-expect-error
message: error2?.message,
error: error2
};
}
message = JSON.stringify(
{
type: "validation",
on: type,
summary: mapValueError(error).summary,
property: accessor,
message: error?.message,
expected,
found: value,
errors: errors2
},
null,
2
);
}
super(message);
this.type = type;
this.validator = validator;
this.value = value;
this.code = "VALIDATION";
this.status = 422;
Object.setPrototypeOf(this, _ValidationError.prototype);
}
get all() {
return "Errors" in this.validator ? [...this.validator.Errors(this.value)].map(mapValueError) : (
// @ts-ignore
[...Value.Errors(this.validator, this.value)].map(mapValueError)
);
}
static simplifyModel(validator) {
let model = "schema" in validator ? validator.schema : validator;
try {
return Value.Create(model);
} catch {
return model;
}
}
get model() {
return _ValidationError.simplifyModel(this.validator);
}
toResponse(headers) {
return new Response(this.message, {
status: 400,
headers: {
...headers,
"content-type": "application/json"
}
});
}
};
// src/parse-query.ts
import decode from "fast-decode-uri-component";
function parseQuery(input) {
let result = /* @__PURE__ */ Object.create(null), flags = 0, KEY_HAS_PLUS = 1, KEY_NEEDS_DECODE = 2, VALUE_HAS_PLUS = 4, VALUE_NEEDS_DECODE = 8, inputLength = input.length, startingIndex = -1, equalityIndex = -1;
for (let i = 0; i < inputLength; i++)
switch (input.charCodeAt(i)) {
// '&'
case 38:
processKeyValuePair(input, i), startingIndex = i, equalityIndex = i, flags = 0;
break;
// '='
case 61:
equalityIndex <= startingIndex ? equalityIndex = i : flags |= VALUE_NEEDS_DECODE;
break;
// '+'
case 43:
equalityIndex > startingIndex ? flags |= VALUE_HAS_PLUS : flags |= KEY_HAS_PLUS;
break;
// '%'
case 37:
equalityIndex > startingIndex ? flags |= VALUE_NEEDS_DECODE : flags |= KEY_NEEDS_DECODE;
break;
}
return startingIndex < inputLength && processKeyValuePair(input, inputLength), result;
function processKeyValuePair(input2, endIndex) {
let hasBothKeyValuePair = equalityIndex > startingIndex, effectiveEqualityIndex = hasBothKeyValuePair ? equalityIndex : endIndex, keySlice = input2.slice(startingIndex + 1, effectiveEqualityIndex);
if (!hasBothKeyValuePair && keySlice.length === 0) return;
let finalKey = keySlice;
flags & KEY_HAS_PLUS && (finalKey = finalKey.replace(/\+/g, " ")), flags & KEY_NEEDS_DECODE && (finalKey = decode(finalKey) || finalKey);
let finalValue = "";
if (hasBothKeyValuePair) {
let valueSlice = input2.slice(equalityIndex + 1, endIndex);
flags & VALUE_HAS_PLUS && (valueSlice = valueSlice.replace(/\+/g, " ")), flags & VALUE_NEEDS_DECODE && (valueSlice = decode(valueSlice) || valueSlice), finalValue = valueSlice;
}
let currentValue = result[finalKey];
currentValue === void 0 ? result[finalKey] = finalValue : Array.isArray(currentValue) ? currentValue.push(finalValue) : result[finalKey] = [currentValue, finalValue];
}
}
// src/cookies.ts
import { parse, serialize } from "cookie";
import decode2 from "fast-decode-uri-component";
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() ?? "";
}
}, createCookieJar = (set, store, initial) => (set.cookie || (set.cookie = {}), new Proxy(store, {
get(_, key) {
return key in store ? new Cookie(
key,
set.cookie,
Object.assign({}, initial ?? {}, store[key])
) : new Cookie(
key,
set.cookie,
Object.assign({}, initial)
);
}
})), parseCookie = async (set, cookieString, {
secrets,
sign,
...initial
} = {}) => {
if (!cookieString) return createCookieJar(set, {}, initial);
let isStringKey = typeof secrets == "string";
sign && sign !== !0 && !Array.isArray(sign) && (sign = [sign]);
let jar = {}, cookies = parse(cookieString);
for (let [name, v] of Object.entries(cookies)) {
if (v === void 0) continue;
let value = decode2(v);
if (sign === !0 || sign?.includes(name)) {
if (!secrets)
throw new Error("No secret is provided to cookie plugin");
if (isStringKey) {
let temp = await unsignCookie(value, secrets);
if (temp === !1) throw new InvalidCookieSignature(name);
value = temp;
} else {
let decoded = !0;
for (let i = 0; i < secrets.length; i++) {
let temp = await unsignCookie(value, secrets[i]);
if (temp !== !1) {
decoded = !0, value = temp;
break;
}
}
if (!decoded) throw new InvalidCookieSignature(name);
}
}
jar[name] = {
value
};
}
return createCookieJar(set, jar, initial);
};
// src/dynamic-handle.ts
var injectDefaultValues = (typeChecker, obj) => {
for (let [key, keySchema] of Object.entries(
// @ts-expect-error private
typeChecker.schema.properties
))
obj[key] ??= keySchema.default;
}, createDynamicHandler = (app) => {
let { mapResponse, mapEarlyResponse } = app["~adapter"].handler;
return async (request) => {
let url = request.url, s = url.indexOf("/", 11), qi = url.indexOf("?", s + 1), path = qi === -1 ? url.substring(s) : url.substring(s, qi), set = {
cookie: {},
status: 200,
headers: {}
}, context = Object.assign(
{},
// @ts-expect-error
app.singleton.decorator,
{
set,
// @ts-expect-error
store: app.singleton.store,
request,
path,
qi,
error: status,
status,
redirect
}
);
try {
if (app.event.request)
for (let i = 0; i < app.event.request.length; i++) {
let onRequest = app.event.request[i].fn, response2 = onRequest(context);
if (response2 instanceof Promise && (response2 = await response2), response2 = mapEarlyResponse(response2, set), response2) return context.response = response2;
}
let methodKey = request.method === "GET" && request.headers.get("upgrade")?.toLowerCase() === "websocket" ? "WS" : request.method, handler = app.router.dynamic.find(request.method, path) ?? app.router.dynamic.find(methodKey, path) ?? app.router.dynamic.find("ALL", path);
if (!handler) throw new NotFoundError();
let { handle, hooks, validator, content, route } = handler.store, body;
if (request.method !== "GET" && request.method !== "HEAD")
if (content)
switch (content) {
case "application/json":
body = await request.json();
break;
case "text/plain":
body = await request.text();
break;
case "application/x-www-form-urlencoded":
body = parseQuery(await request.text());
break;
case "application/octet-stream":
body = await request.arrayBuffer();
break;
case "multipart/form-data":
body = {};
let form = await request.formData();
for (let key of form.keys()) {
if (body[key]) continue;
let value = form.getAll(key);
value.length === 1 ? body[key] = value[0] : body[key] = value;
}
break;
}
else {
let contentType = request.headers.get("content-type");
if (contentType) {
let index = contentType.indexOf(";");
if (index !== -1 && (contentType = contentType.slice(0, index)), context.contentType = contentType, hooks.parse)
for (let i = 0; i < hooks.parse.length; i++) {
let hook = hooks.parse[i].fn, temp = hook(context, contentType);
if (temp instanceof Promise && (temp = await temp), temp) {
body = temp;
break;
}
}
if (delete context.contentType, body === void 0)
switch (contentType) {
case "application/json":
body = await request.json();
break;
case "text/plain":
body = await request.text();
break;
case "application/x-www-form-urlencoded":
body = parseQuery(await request.text());
break;
case "application/octet-stream":
body = await request.arrayBuffer();
break;
case "multipart/form-data":
body = {};
let form = await request.formData();
for (let key of form.keys()) {
if (body[key]) continue;
let value = form.getAll(key);
value.length === 1 ? body[key] = value[0] : body[key] = value;
}
break;
}
}
}
context.route = route, context.body = body, context.params = handler?.params || void 0, context.query = qi === -1 ? {} : parseQuery(url.substring(qi + 1)), context.headers = {};
for (let [key, value] of request.headers.entries())
context.headers[key] = value;
let cookieMeta = Object.assign(
{},
app.config?.cookie,
validator?.cookie?.config
), cookieHeaderValue = request.headers.get("cookie");
context.cookie = await parseCookie(
context.set,
cookieHeaderValue,
cookieMeta ? {
secrets: cookieMeta.secrets !== void 0 ? typeof cookieMeta.secrets == "string" ? cookieMeta.secrets : cookieMeta.secrets.join(",") : void 0,
sign: cookieMeta.sign === !0 ? !0 : cookieMeta.sign !== void 0 ? typeof cookieMeta.sign == "string" ? cookieMeta.sign : cookieMeta.sign.join(",") : void 0
} : void 0
);
let headerValidator = validator?.createHeaders?.();
headerValidator && injectDefaultValues(headerValidator, context.headers);
let paramsValidator = validator?.createParams?.();
paramsValidator && injectDefaultValues(paramsValidator, context.params);
let queryValidator = validator?.createQuery?.();
if (queryValidator && injectDefaultValues(queryValidator, context.query), hooks.transform)
for (let i = 0; i < hooks.transform.length; i++) {
let hook = hooks.transform[i], response2 = hook.fn(context);
if (response2 instanceof Promise && (response2 = await response2), response2 instanceof ElysiaCustomStatusResponse) {
let result = mapEarlyResponse(response2, context.set);
if (result)
return context.response = result;
}
hook.subType === "derive" && Object.assign(context, response2);
}
if (validator) {
if (headerValidator) {
let _header = structuredClone(context.headers);
for (let [key, value] of request.headers)
_header[key] = value;
if (validator.headers.Check(_header) === !1)
throw new ValidationError(
"header",
validator.headers,
_header
);
} else validator.headers?.Decode && (context.headers = validator.headers.Decode(context.headers));
if (paramsValidator?.Check(context.params) === !1)
throw new ValidationError(
"params",
validator.params,
context.params
);
if (validator.params?.Decode && (context.params = validator.params.Decode(context.params)), queryValidator?.Check(context.query) === !1)
throw new ValidationError(
"query",
validator.query,
context.query
);
if (validator.query?.Decode && (context.query = validator.query.Decode(context.query)), validator.createCookie?.()) {
let cookieValue = {};
for (let [key, value] of Object.entries(context.cookie))
cookieValue[key] = value.value;
if (validator.cookie.Check(cookieValue) === !1)
throw new ValidationError(
"cookie",
validator.cookie,
cookieValue
);
validator.cookie?.Decode && (cookieValue = validator.cookie.Decode(
cookieValue
));
}
if (validator.createBody?.()?.Check(body) === !1)
throw new ValidationError("body", validator.body, body);
validator.body?.Decode && (context.body = validator.body.Decode(body));
}
if (hooks.beforeHandle)
for (let i = 0; i < hooks.beforeHandle.length; i++) {
let hook = hooks.beforeHandle[i], response2 = hook.fn(context);
if (response2 instanceof Promise && (response2 = await response2), response2 instanceof ElysiaCustomStatusResponse) {
let result = mapEarlyResponse(response2, context.set);
if (result)
return context.response = result;
}
if (hook.subType === "resolve") {
Object.assign(context, response2);
continue;
}
if (response2 !== void 0) {
if (context.response = response2, hooks.afterHandle)
for (let i2 = 0; i2 < hooks.afterHandle.length; i2++) {
let newResponse = hooks.afterHandle[i2].fn(
context
);
newResponse instanceof Promise && (newResponse = await newResponse), newResponse && (response2 = newResponse);
}
let result = mapEarlyResponse(response2, context.set);
if (result) return context.response = result;
}
}
let response = typeof handle == "function" ? handle(context) : handle;
if (response instanceof Promise && (response = await response), hooks.afterHandle)
if (hooks.afterHandle.length) {
context.response = response;
for (let i = 0; i < hooks.afterHandle.length; i++) {
let newResponse = hooks.afterHandle[i].fn(
context
);
newResponse instanceof Promise && (newResponse = await newResponse);
let result = mapEarlyResponse(
newResponse,
context.set
);
if (result !== void 0) {
let responseValidator = (
// @ts-expect-error
validator?.response?.[result.status]
);
if (responseValidator?.Check(result) === !1)
throw new ValidationError(
"response",
responseValidator,
result
);
return responseValidator?.Encode && (response = responseValidator.Encode(response)), context.response = result;
}
}
} else {
let status2 = response instanceof ElysiaCustomStatusResponse ? response.code : set.status ? typeof set.status == "string" ? StatusMap[set.status] : set.status : 200, responseValidator = validator?.createResponse?.()?.[status2];
if (responseValidator?.Check(response) === !1)
throw new ValidationError(
"response",
responseValidator,
response
);
responseValidator?.Encode && (response = responseValidator.Encode(response));
}
if (context.set.cookie && cookieMeta?.sign) {
let secret = cookieMeta.secrets ? typeof cookieMeta.secrets == "string" ? cookieMeta.secrets : cookieMeta.secrets[0] : void 0;
if (cookieMeta.sign === !0) {
if (secret)
for (let [key, cookie] of Object.entries(
context.set.cookie
))
context.set.cookie[key].value = await signCookie(
cookie.value,
secret
);
} else {
let properties = validator?.cookie?.schema?.properties;
if (secret)
for (let name of cookieMeta.sign)
name in properties && context.set.cookie[name]?.value && (context.set.cookie[name].value = await signCookie(
context.set.cookie[name].value,
secret
));
}
}
return mapResponse(context.response = response, context.set);
} catch (error) {
let reportedError = error instanceof TransformDecodeError && error.error ? error.error : error;
return app.handleError(context, reportedError);
} finally {
if (app.event.afterResponse)
for (let afterResponse of app.event.afterResponse)
await afterResponse.fn(context);
}
};
}, createDynamicErrorHandler = (app) => {
let { mapResponse } = app["~adapter"].handler;
return async (context, error) => {
let errorContext = Object.assign(context, { error, code: error.code });
if (errorContext.set = context.set, app.event.error)
for (let i = 0; i < app.event.error.length; i++) {
let response = app.event.error[i].fn(errorContext);
if (response instanceof Promise && (response = await response), response != null)
return context.response = mapResponse(
response,
context.set
);
}
return new Response(
typeof error.cause == "string" ? error.cause : error.message,
{
headers: context.set.headers,
status: error.status ?? 500
}
);
};
};
export {
createDynamicErrorHandler,
createDynamicHandler
};