Project quality audit (#2758)

* Harden security, health checks, and dependency hygiene

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>

* Finalize health and storage hardening adjustments

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>

* remove use of [REDACTED]

* update dependencies

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Amruth Pillai
2026-02-28 01:14:02 +01:00
committed by GitHub
parent c25c068d20
commit b7e4c86f4e
21 changed files with 1313 additions and 3328 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ description: "Learn how to create API keys and authenticate requests to the Reac
</Info>
```bash
curl "https://rxresu.me/api/openapi/resume/list" \
curl "https://rxresu.me/api/openapi/resumes" \
-H "x-api-key: YOUR_API_KEY"
```
</Step>
+21 -16
View File
@@ -31,11 +31,11 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.47",
"@ai-sdk/anthropic": "^3.0.49",
"@ai-sdk/google": "^3.0.33",
"@ai-sdk/openai": "^3.0.36",
"@ai-sdk/react": "^3.0.105",
"@aws-sdk/client-s3": "^3.999.0",
"@ai-sdk/react": "^3.0.106",
"@aws-sdk/client-s3": "^3.1000.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -56,17 +56,17 @@
"@sindresorhus/slugify": "^3.0.0",
"@t3-oss/env-core": "^0.13.10",
"@tanstack/react-query": "5.90.21",
"@tanstack/react-router": "^1.163.2",
"@tanstack/react-router-ssr-query": "^1.163.2",
"@tanstack/react-start": "^1.163.2",
"@tanstack/zod-adapter": "^1.163.2",
"@tanstack/react-router": "^1.163.3",
"@tanstack/react-router-ssr-query": "^1.163.3",
"@tanstack/react-start": "^1.163.3",
"@tanstack/zod-adapter": "^1.163.3",
"@tiptap/extension-highlight": "^3.20.0",
"@tiptap/extension-table": "^3.20.0",
"@tiptap/extension-text-align": "^3.20.0",
"@tiptap/pm": "^3.20.0",
"@tiptap/react": "^3.20.0",
"@tiptap/starter-kit": "^3.20.0",
"ai": "^6.0.103",
"ai": "^6.0.104",
"ai-sdk-ollama": "^3.8.0",
"bcrypt": "^6.0.0",
"better-auth": "v1.4.19",
@@ -97,7 +97,6 @@
"react-resizable-panels": "^4.6.5",
"react-window": "^2.2.7",
"react-zoom-pan-pinch": "^3.7.0",
"shadcn": "^3.8.5",
"sharp": "^0.34.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
@@ -130,24 +129,30 @@
"drizzle-kit": "^1.0.0-beta.15-859cf75",
"knip": "^5.85.0",
"nitro": "npm:nitro-nightly@latest",
"node-addon-api": "^8.5.0",
"node-addon-api": "^8.6.0",
"node-gyp": "^12.2.0",
"npm-check-updates": "^19.6.2",
"npm-check-updates": "^19.6.3",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^8.0.0-beta.15",
"vite": "^8.0.0-beta.16",
"vite-plugin-pwa": "^1.2.0"
},
"pnpm": {
"overrides": {
"vite": "^8.0.0-beta.15"
"vite": "^8.0.0-beta.16",
"hono": "^4.11.10",
"minimatch": "^10.2.3",
"@isaacs/brace-expansion": "^5.0.1",
"basic-ftp": "^5.2.0",
"lodash": "^4.17.23",
"markdown-it": "^14.1.1",
"ajv": "^8.18.0",
"qs": "^6.14.2",
"fast-xml-parser": "^5.3.8"
},
"onlyBuiltDependencies": [
"@prisma/engines",
"bcrypt",
"esbuild",
"msw",
"prisma",
"sharp"
]
}
+1
View File
@@ -20,6 +20,7 @@ async function migrateDatabase() {
console.log("✅ Database migrations completed");
} catch (error) {
console.error("🚨 Database migrations failed:", error);
throw error;
} finally {
await pool.end();
}
+1010 -3245
View File
File diff suppressed because it is too large Load Diff
+15 -8
View File
@@ -1,5 +1,6 @@
import nodemailer, { type Transporter } from "nodemailer";
import { env } from "@/utils/env";
import { logger } from "@/utils/logger";
type SendEmailOptions = {
to: string | string[];
@@ -44,18 +45,24 @@ export const sendEmail = async (options: SendEmailOptions) => {
text: options.text,
};
if (!transport) return console.log("[EMAIL] SMTP not configured; logging email:", payload);
if (!transport) {
logger.info("SMTP not configured; skipping email send", {
to: payload.to,
subject: payload.subject,
});
return;
}
try {
await transport.sendMail({ ...options, from });
} catch (error) {
console.error(
"[EMAIL] Failed to send via SMTP; logging email payload instead:",
{
smtp: { host: env.SMTP_HOST, port: env.SMTP_PORT, secure: env.SMTP_SECURE },
email: payload,
},
logger.error("SMTP send failed", {
smtpHost: env.SMTP_HOST,
smtpPort: env.SMTP_PORT,
smtpSecure: env.SMTP_SECURE,
to: payload.to,
subject: payload.subject,
error,
);
});
}
};
+9 -2
View File
@@ -7,13 +7,17 @@ import { createIsomorphicFn } from "@tanstack/react-start";
import { getRequestHeaders } from "@tanstack/react-start/server";
import router from "@/integrations/orpc/router";
import { getLocale } from "@/utils/locale";
import { logger } from "@/utils/logger";
export const getORPCClient = createIsomorphicFn()
.server((): RouterClient<typeof router> => {
return createRouterClient(router, {
interceptors: [
onError((error) => {
console.error(`ERROR [oRPC]: ${error}`);
logger.error("oRPC server client error", {
scope: "server",
error,
});
}),
],
context: async () => {
@@ -40,7 +44,10 @@ export const getORPCClient = createIsomorphicFn()
interceptors: [
onError((error) => {
if (error instanceof DOMException && error.name === "AbortError") return;
console.error(`ERROR [oRPC]: ${error}`);
logger.error("oRPC browser client error", {
scope: "client",
error,
});
}),
],
});
+2 -1
View File
@@ -70,10 +70,11 @@ export const protectedProcedure = publicProcedure.use(async ({ context, next })
*/
export const serverOnlyProcedure = publicProcedure.use(async ({ context, next }) => {
const headers = context.reqHeaders ?? new Headers();
const isDebugBypassEnabled = env.FLAG_DEBUG_PRINTER && process.env.NODE_ENV === "development";
// Check for the custom header that indicates this is a server-side call
// Server-side calls using createRouterClient have this header set
const isServerSideCall = env.FLAG_DEBUG_PRINTER || headers.get("x-server-side-call") === "true";
const isServerSideCall = isDebugBypassEnabled || headers.get("x-server-side-call") === "true";
// If the header is not present, this is a client-side HTTP request - reject it
if (!isServerSideCall) {
+7 -3
View File
@@ -18,7 +18,7 @@ export const printerRouter = {
.input(z.object({ id: z.string().describe("The unique identifier of the resume to export.") }))
.output(z.object({ url: z.string().describe("The URL to download the generated PDF file.") }))
.handler(async ({ input, context }) => {
const { id, data, userId } = await resumeService.getByIdForPrinter({ id: input.id });
const { id, data, userId } = await resumeService.getByIdForPrinter({ id: input.id, userId: context.user?.id });
const url = await printerService.printResumeAsPDF({ id, data, userId });
if (!context.user) {
@@ -41,9 +41,13 @@ export const printerRouter = {
})
.input(z.object({ id: z.string().describe("The unique identifier of the resume.") }))
.output(z.object({ url: z.string().nullable().describe("The URL to the screenshot image, or null.") }))
.handler(async ({ input }) => {
.handler(async ({ context, input }) => {
try {
const { id, data, userId, updatedAt } = await resumeService.getByIdForPrinter({ id: input.id });
const { id, data, userId, updatedAt } = await resumeService.getByIdForPrinter({
id: input.id,
userId: context.user.id,
});
const url = await printerService.getResumeScreenshot({ id, data, userId, updatedAt });
return { url };
+21 -5
View File
@@ -11,6 +11,14 @@ const filenameSchema = z.object({
filename: z.string().min(1).describe("The path or filename of the file to delete."),
});
function normalizeKey(input: string): string {
return input.trim().replace(/^\/+/, "").split("/").filter(Boolean).join("/");
}
function isUnsafeStorageKey(key: string): boolean {
return key.split("/").some((segment) => segment === "." || segment === "..");
}
export const storageRouter = {
uploadFile: protectedProcedure
.route({
@@ -76,13 +84,21 @@ export const storageRouter = {
message: "The specified file was not found in storage.",
status: 404,
},
FORBIDDEN: {
message: "You do not have permission to delete this file.",
status: 403,
},
})
.handler(async ({ context, input }): Promise<void> => {
// The filename is now the full path from the URL (e.g., "uploads/userId/pictures/timestamp.ext")
// We need to extract just the path portion that matches the storage key
const key = input.filename.startsWith("uploads/")
? input.filename
: `uploads/${context.user.id}/pictures/${input.filename}`;
const requestedKey = normalizeKey(input.filename);
const key = requestedKey.startsWith("uploads/")
? requestedKey
: normalizeKey(`uploads/${context.user.id}/pictures/${requestedKey}`);
const userPrefix = `uploads/${context.user.id}/`;
if (isUnsafeStorageKey(key) || !key.startsWith(userPrefix)) {
throw new ORPCError("FORBIDDEN");
}
const deleted = await storageService.delete(key);
+4 -1
View File
@@ -40,6 +40,9 @@ type GetModelInput = {
baseURL: string;
};
const MAX_AI_FILE_BYTES = 10 * 1024 * 1024; // 10MB
const MAX_AI_FILE_BASE64_CHARS = Math.ceil((MAX_AI_FILE_BYTES * 4) / 3) + 4;
function getModel(input: GetModelInput) {
const { provider, model, apiKey } = input;
const baseURL = input.baseURL || undefined;
@@ -62,7 +65,7 @@ export const aiCredentialsSchema = z.object({
export const fileInputSchema = z.object({
name: z.string(),
data: z.string(), // base64 encoded
data: z.string().max(MAX_AI_FILE_BASE64_CHARS, "File is too large. Maximum size is 10MB."), // base64 encoded
});
type TestConnectionInput = z.infer<typeof aiCredentialsSchema>;
+5 -1
View File
@@ -4,6 +4,7 @@ import type { AuthProvider } from "@/integrations/auth/types";
import { schema } from "@/integrations/drizzle";
import { db } from "@/integrations/drizzle/client";
import { env } from "@/utils/env";
import { logger } from "@/utils/logger";
import { getStorageService } from "./storage";
export type ProviderList = Partial<Record<AuthProvider, string>>;
@@ -39,7 +40,10 @@ export const authService = {
try {
await db.delete(schema.user).where(eq(schema.user.id, input.userId));
} catch (err) {
console.error(`Failed to delete user record for userId=${input.userId}:`, err);
logger.error("Failed to delete user record", {
userId: input.userId,
error: err,
});
throw new ORPCError("INTERNAL_SERVER_ERROR");
}
+9 -7
View File
@@ -1,6 +1,6 @@
import { ORPCError } from "@orpc/server";
import type { InferSelectModel } from "drizzle-orm";
import puppeteer, { type Browser, type ConnectOptions } from "puppeteer-core";
import puppeteer, { type Browser, type ConnectOptions, type Page } from "puppeteer-core";
import type { schema } from "@/integrations/drizzle";
import { pageDimensionsAsPixels } from "@/schema/page";
import { printMarginTemplates } from "@/schema/templates";
@@ -111,6 +111,7 @@ export const printerService = {
}
let browser: Browser | null = null;
let page: Page | null = null;
try {
// Step 4: Connect to the browser and navigate to the printer route
@@ -119,7 +120,7 @@ export const printerService = {
// Set locale cookie so the resume renders in the correct language
await browser.setCookie({ name: "locale", value: locale, domain });
const page = await browser.newPage();
page = await browser.newPage();
// Wait for the page to fully load (network idle + custom loaded attribute)
await page.emulateMediaType("print");
@@ -213,8 +214,6 @@ export const printerService = {
},
});
await page.close();
// Step 7: Upload the generated PDF to storage
const result = await uploadFile({
userId,
@@ -227,6 +226,8 @@ export const printerService = {
return result.url;
} catch (error) {
throw new ORPCError("INTERNAL_SERVER_ERROR", error as Error);
} finally {
if (page) await page.close().catch(() => null);
}
},
@@ -280,13 +281,14 @@ export const printerService = {
const url = `${baseUrl}/printer/${id}?token=${token}`;
let browser: Browser | null = null;
let page: Page | null = null;
try {
browser = await getBrowser();
await browser.setCookie({ name: "locale", value: locale, domain });
const page = await browser.newPage();
page = await browser.newPage();
await page.setViewport(pageDimensionsAsPixels.a4);
await page.goto(url, { waitUntil: "networkidle0" });
@@ -294,8 +296,6 @@ export const printerService = {
const screenshotBuffer = await page.screenshot({ type: "webp", quality: 80 });
await page.close();
const result = await uploadFile({
userId,
resumeId: id,
@@ -307,6 +307,8 @@ export const printerService = {
return result.url;
} catch (error) {
throw new ORPCError("INTERNAL_SERVER_ERROR", error as Error);
} finally {
if (page) await page.close().catch(() => null);
}
},
};
+6 -2
View File
@@ -133,7 +133,7 @@ export const resumeService = {
return resume;
},
getByIdForPrinter: async (input: { id: string }) => {
getByIdForPrinter: async (input: { id: string; userId?: string }) => {
const [resume] = await db
.select({
id: schema.resume.id,
@@ -146,7 +146,11 @@ export const resumeService = {
updatedAt: schema.resume.updatedAt,
})
.from(schema.resume)
.where(eq(schema.resume.id, input.id));
.where(
input.userId
? and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId))
: eq(schema.resume.id, input.id),
);
if (!resume) throw new ORPCError("NOT_FOUND");
+31 -6
View File
@@ -6,6 +6,8 @@ import { db } from "@/integrations/drizzle/client";
const CACHE_DURATION_MS = 6 * 60 * 60 * 1000; // 6 hours
const GITHUB_API_URL = "https://api.github.com/repos/amruthpillai/reactive-resume";
const GITHUB_REQUEST_TIMEOUT_MS = 5_000;
const GITHUB_REQUEST_MAX_ATTEMPTS = 2;
const LAST_KNOWN = {
users: 978_528,
@@ -65,13 +67,36 @@ const getCountFromDatabase = async (table: typeof schema.user | typeof schema.re
return result.count;
};
const getGitHubStars = async (): Promise<number | null> => {
const response = await fetch(GITHUB_API_URL, { headers: { Accept: "application/vnd.github+json" } });
if (!response.ok) return null;
const fetchGitHubStarsOnce = async (): Promise<number | null> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), GITHUB_REQUEST_TIMEOUT_MS);
const data = await response.json();
const stars = Number(data.stargazers_count);
return Number.isFinite(stars) && stars > 0 ? stars : null;
try {
const response = await fetch(GITHUB_API_URL, {
signal: controller.signal,
headers: {
Accept: "application/vnd.github+json",
},
});
if (!response.ok) return null;
const data = (await response.json()) as { stargazers_count?: unknown };
const stars = Number(data.stargazers_count);
return Number.isFinite(stars) && stars > 0 ? stars : null;
} catch {
return null;
} finally {
clearTimeout(timeoutId);
}
};
const getGitHubStars = async (): Promise<number | null> => {
for (let attempt = 0; attempt < GITHUB_REQUEST_MAX_ATTEMPTS; attempt++) {
const stars = await fetchGitHubStarsOnce();
if (stars !== null) return stars;
}
return null;
};
export const statisticsService = {
+20 -9
View File
@@ -8,6 +8,7 @@ import {
S3Client,
} from "@aws-sdk/client-s3";
import { env } from "@/utils/env";
import { logger } from "@/utils/logger";
interface StorageWriteInput {
key: string;
@@ -89,7 +90,9 @@ interface ProcessedImage {
export async function processImageForUpload(file: File): Promise<ProcessedImage> {
const fileBuffer = await file.arrayBuffer();
console.log("FLAG_DISABLE_IMAGE_PROCESSING", env.FLAG_DISABLE_IMAGE_PROCESSING);
logger.debug("Image processing feature flag resolved", {
flagDisableImageProcessing: env.FLAG_DISABLE_IMAGE_PROCESSING,
});
if (env.FLAG_DISABLE_IMAGE_PROCESSING) {
return {
data: new Uint8Array(fileBuffer),
@@ -143,15 +146,23 @@ class LocalStorageService implements StorageService {
async read(key: string): Promise<StorageReadResult | null> {
const fullPath = this.resolvePath(key);
const [arrayBuffer, stats] = await Promise.all([fs.readFile(fullPath), fs.stat(fullPath)]);
try {
const [arrayBuffer, stats] = await Promise.all([fs.readFile(fullPath), fs.stat(fullPath)]);
return {
data: arrayBuffer,
size: stats.size,
etag: `"${stats.size}-${stats.mtime.getTime()}"`,
lastModified: stats.mtime,
contentType: inferContentType(key),
};
return {
data: arrayBuffer,
size: stats.size,
etag: `"${stats.size}-${stats.mtime.getTime()}"`,
lastModified: stats.mtime,
contentType: inferContentType(key),
};
} catch (error: unknown) {
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
return null;
}
throw error;
}
}
async delete(key: string): Promise<boolean> {
+67 -17
View File
@@ -3,30 +3,80 @@ import { sql } from "drizzle-orm";
import { db } from "@/integrations/drizzle/client";
import { printerService } from "@/integrations/orpc/services/printer";
import { getStorageService } from "@/integrations/orpc/services/storage";
import { logger } from "@/utils/logger";
function isUnhealthy(check: unknown): boolean {
return (
!!check &&
typeof check === "object" &&
"status" in check &&
typeof check.status === "string" &&
check.status === "unhealthy"
);
const HEALTHCHECK_TIMEOUT_MS = 1_500;
type CheckResult = {
status: "healthy" | "unhealthy";
latencyMs: number;
error?: string;
[key: string]: unknown;
};
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
return "Unknown error";
}
async function handler() {
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
let timeoutId: NodeJS.Timeout | undefined;
const timeout = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
});
try {
return await Promise.race([promise, timeout]);
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
async function runCheck(check: () => Promise<object>): Promise<CheckResult> {
const startedAt = performance.now();
try {
const data = await withTimeout(check(), HEALTHCHECK_TIMEOUT_MS);
const latencyMs = Math.round(performance.now() - startedAt);
const result = data as { status?: string };
if (result.status === "unhealthy") return { ...(data as object), status: "unhealthy", latencyMs };
return { ...(data as object), status: "healthy", latencyMs };
} catch (error) {
return {
status: "unhealthy",
error: getErrorMessage(error),
latencyMs: Math.round(performance.now() - startedAt),
};
}
}
async function healthHandler() {
const [database, printer, storage] = await Promise.all([
runCheck(checkDatabase),
runCheck(checkPrinter),
runCheck(checkStorage),
]);
const status = [database, printer, storage].some((check) => check.status === "unhealthy") ? "unhealthy" : "healthy";
const checks = {
service: "reactive-resume",
version: process.env.npm_package_version,
status: "healthy",
status,
timestamp: new Date().toISOString(),
uptime: `${process.uptime().toFixed(2)}s`,
database: await checkDatabase(),
printer: await checkPrinter(),
storage: await checkStorage(),
database,
printer,
storage,
};
if (checks.status === "unhealthy" || Object.values(checks).some(isUnhealthy)) {
checks.status = "unhealthy";
if (status === "unhealthy") {
logger.warn("Healthcheck failed", {
route: "/api/health",
database,
printer,
storage,
});
}
const headers = new Headers();
@@ -36,7 +86,7 @@ async function handler() {
return new Response(body, {
headers,
status: checks.status === "unhealthy" ? 500 : 200,
status: checks.status === "unhealthy" ? 503 : 200,
});
}
@@ -80,7 +130,7 @@ async function checkStorage() {
export const Route = createFileRoute("/api/health")({
server: {
handlers: {
GET: handler,
GET: healthHandler,
},
},
});
+5 -1
View File
@@ -9,6 +9,7 @@ import router from "@/integrations/orpc/router";
import { resumeDataSchema } from "@/schema/resume/data";
import { env } from "@/utils/env";
import { getLocale } from "@/utils/locale";
import { logger } from "@/utils/logger";
const openAPIHandler = new OpenAPIHandler(router, {
plugins: [
@@ -21,7 +22,10 @@ const openAPIHandler = new OpenAPIHandler(router, {
],
interceptors: [
onError((error) => {
console.error(`ERROR [OpenAPI]: ${error}`);
logger.error("OpenAPI handler error", {
route: "/api/openapi",
error,
});
}),
],
});
+5 -1
View File
@@ -4,12 +4,16 @@ import { BatchHandlerPlugin, RequestHeadersPlugin, StrictGetMethodPlugin } from
import { createFileRoute } from "@tanstack/react-router";
import router from "@/integrations/orpc/router";
import { getLocale } from "@/utils/locale";
import { logger } from "@/utils/logger";
const rpcHandler = new RPCHandler(router, {
plugins: [new BatchHandlerPlugin(), new RequestHeadersPlugin(), new StrictGetMethodPlugin()],
interceptors: [
onError((error) => {
console.error(`ERROR [oRPC]: ${error}`);
logger.error("oRPC server error", {
route: "/api/rpc",
error,
});
}),
],
});
+5 -1
View File
@@ -1,6 +1,7 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { createFileRoute } from "@tanstack/react-router";
import { logger } from "@/utils/logger";
import { registerPrompts } from "./-helpers/prompts";
import { registerResources } from "./-helpers/resources";
import { registerTools } from "./-helpers/tools";
@@ -51,7 +52,10 @@ export const Route = createFileRoute("/mcp/")({
return await transport.handleRequest(request);
} catch (error) {
console.error(`Error handling request: ${error instanceof Error ? error.message : String(error)}`);
logger.error("MCP request failed", {
route: "/mcp",
error,
});
return Response.json({
id: null,
-1
View File
@@ -2,7 +2,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
+69
View File
@@ -0,0 +1,69 @@
type LogLevel = "debug" | "info" | "warn" | "error";
type LogContext = Record<string, unknown> & {
error?: unknown;
};
type SerializedError = {
name: string;
message: string;
stack?: string;
};
function serializeError(error: unknown): SerializedError | undefined {
if (!error) return undefined;
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: error.stack,
};
}
if (typeof error === "string") {
return {
name: "Error",
message: error,
};
}
if (typeof error === "object") {
return {
name: "Error",
message: JSON.stringify(error),
};
}
return {
name: "Error",
message: String(error),
};
}
function log(level: LogLevel, message: string, context: LogContext = {}) {
const { error, ...rest } = context;
const payload = {
ts: new Date().toISOString(),
level,
message,
...rest,
error: serializeError(error),
};
const json = JSON.stringify(payload);
if (level === "error") {
console.error(json);
return;
}
if (level === "warn") {
console.warn(json);
return;
}
console.log(json);
}
export const logger = {
debug: (message: string, context?: LogContext) => log("debug", message, context),
info: (message: string, context?: LogContext) => log("info", message, context),
warn: (message: string, context?: LogContext) => log("warn", message, context),
error: (message: string, context?: LogContext) => log("error", message, context),
};