refactor: 🚚 Organize code into sub-packages, instead of a single large package

This commit is contained in:
Jesse Wierzbinski 2025-06-15 04:38:20 +02:00
parent 79742f47dc
commit a6d3ebbeef
No known key found for this signature in database
366 changed files with 942 additions and 833 deletions

View file

@ -0,0 +1,16 @@
import { ApiError } from "@versia/kit";
import { config } from "@versia-server/config";
import { createMiddleware } from "hono/factory";
export const agentBans = createMiddleware(async (context, next) => {
// Check for banned user agents (regex)
const ua = context.req.header("user-agent") ?? "";
for (const agent of config.http.banned_user_agents) {
if (new RegExp(agent).test(ua)) {
throw new ApiError(403, "Forbidden");
}
}
await next();
});

View file

@ -0,0 +1,20 @@
import { ApiError } from "@versia/kit";
import { createMiddleware } from "hono/factory";
export const boundaryCheck = createMiddleware(async (context, next) => {
// Checks that FormData boundary is present
const contentType = context.req.header("content-type");
if (
contentType?.includes("multipart/form-data") &&
!contentType.includes("boundary")
) {
throw new ApiError(
400,
"Missing FormData boundary",
"You are sending a request with a multipart/form-data content type but without a boundary. Please include a boundary in the Content-Type header. For more information, visit https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data",
);
}
await next();
});

View file

@ -0,0 +1,40 @@
import { getLogger } from "@logtape/logtape";
import { ApiError } from "@versia/kit";
import { config } from "@versia-server/config";
import type { SocketAddress } from "bun";
import { createMiddleware } from "hono/factory";
import { matches } from "ip-matching";
import { sentry } from "@/sentry";
export const ipBans = createMiddleware(async (context, next) => {
// Check for banned IPs
const requestIp = context.env?.ip as SocketAddress | undefined | null;
if (!requestIp?.address) {
await next();
return;
}
for (const ip of config.http.banned_ips) {
try {
if (matches(ip, requestIp?.address)) {
throw new ApiError(403, "Forbidden");
}
} catch (e) {
const logger = getLogger("server");
logger.error`Error while parsing banned IP "${ip}" `;
logger.error`${e}`;
sentry?.captureException(e);
return context.json(
{ error: `A server error occured: ${(e as Error).message}` },
500,
);
}
}
await next();
return;
});

View file

@ -0,0 +1,37 @@
import { getLogger } from "@logtape/logtape";
import { config } from "@versia-server/config";
import { SHA256 } from "bun";
import chalk from "chalk";
import { createMiddleware } from "hono/factory";
export const logger = createMiddleware(async (context, next) => {
if (config.logging.types.requests) {
const serverLogger = getLogger("server");
const body = await context.req.raw.clone().text();
const urlAndMethod = `${chalk.green(context.req.method)} ${chalk.blue(context.req.url)}`;
const hash = `${chalk.bold("Hash")}: ${chalk.yellow(
new SHA256().update(body).digest("hex"),
)}`;
const headers = `${chalk.bold("Headers")}:\n${Array.from(
context.req.raw.headers.entries(),
)
.map(
([key, value]) =>
` - ${chalk.cyan(key)}: ${chalk.white(value)}`,
)
.join("\n")}`;
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
if (config.logging.types.requests_content) {
serverLogger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`;
} else {
serverLogger.debug`${urlAndMethod}`;
}
}
await next();
});

View file

@ -0,0 +1,40 @@
import type { ApiError } from "@versia/kit";
import { env } from "bun";
import type { MiddlewareHandler } from "hono";
import { rateLimiter } from "hono-rate-limiter";
import type { z } from "zod";
import type { HonoEnv } from "~/types/api";
// Not exported by hono-rate-limiter
// So we define it ourselves
type RateLimitEnv = HonoEnv & {
Variables: {
rateLimit: {
limit: number;
remaining: number;
resetTime: Date;
};
};
};
export const rateLimit = (
limit: number,
windowMs = 60 * 1000,
): MiddlewareHandler<RateLimitEnv> =>
env.DISABLE_RATE_LIMIT === "true"
? (_, next): Promise<void> => next()
: rateLimiter<RateLimitEnv>({
keyGenerator: (c): string => c.req.path,
message: (c): z.infer<typeof ApiError.zodSchema> => ({
error: "Too many requests, please try again later.",
details: {
limit: c.get("rateLimit").limit,
remaining: c.get("rateLimit").remaining,
reset: c.get("rateLimit").resetTime.toISOString(),
resetInMs:
c.get("rateLimit").resetTime.getTime() - Date.now(),
},
}),
windowMs,
limit,
});

View file

@ -0,0 +1,19 @@
import { config } from "@versia-server/config";
import { createMiddleware } from "hono/factory";
export const urlCheck = createMiddleware(async (context, next) => {
// Check that request URL matches base_url
const baseUrl = config.http.base_url;
if (new URL(context.req.url).origin !== baseUrl.origin) {
return context.json(
{
error: `Request URL ${context.req.url} does not match base URL ${baseUrl.origin}`,
},
400,
);
}
await next();
return;
});