hf / server.js
incognitolm
ui and stuff
bb2c539
const http = require("node:http");
const path = require("node:path");
const fs = require("node:fs/promises");
const { URL } = require("node:url");
const { SessionStore } = require("./src/session-store");
const {
HfApiError,
buildSpaceStreamUrl,
createResource,
deleteResource,
getDownloadBlob,
getPageData,
getResourceUpdates,
getSearchResults,
getViewer,
moveResource,
performSpaceAction,
removeFile,
saveSpaceSecret,
saveSpaceVariable,
updateTextFile,
updateResourceVisibility,
} = require("./src/hf-api");
const { renderAppShell, renderLoginPage } = require("./src/templates");
const HOST = "0.0.0.0";
const PORT = Number(process.env.PORT || 7860);
const SESSION_COOKIE_NAME = "hf_auth";
const STATIC_DIR = path.join(__dirname, "public");
const SESSION_STORE_DIR = process.env.SESSION_STORE_DIR || "/data";
const SESSION_TTL_HOURS = Number(process.env.SESSION_TTL_HOURS || 168);
const REQUEST_BODY_LIMIT = 1024 * 1024;
const SECURITY_HEADERS = {
"Referrer-Policy": "same-origin",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Permissions-Policy": "camera=(), geolocation=(), microphone=()",
"Content-Security-Policy":
"default-src 'self'; img-src 'self' https: data:; style-src 'self'; script-src 'self'; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'",
};
if (!process.env.ENCRYPTION_KEY) {
throw new Error("ENCRYPTION_KEY is required.");
}
if (!Number.isFinite(SESSION_TTL_HOURS) || SESSION_TTL_HOURS <= 0) {
throw new Error("SESSION_TTL_HOURS must be a positive number.");
}
const sessionStore = new SessionStore({
storageDir: SESSION_STORE_DIR,
encryptionKey: process.env.ENCRYPTION_KEY,
sessionTtlMs: SESSION_TTL_HOURS * 60 * 60 * 1000,
});
const staticCache = new Map();
const mimeTypes = {
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
};
function parseCookies(cookieHeader = "") {
return cookieHeader
.split(";")
.map((part) => part.trim())
.filter(Boolean)
.reduce((cookies, pair) => {
const separator = pair.indexOf("=");
if (separator === -1) {
return cookies;
}
const name = pair.slice(0, separator).trim();
const value = pair.slice(separator + 1).trim();
try {
cookies[name] = decodeURIComponent(value);
} catch (error) {
cookies[name] = value;
}
return cookies;
}, {});
}
function buildCookie(name, value, options = {}) {
const parts = [`${name}=${encodeURIComponent(value)}`];
parts.push(`Path=${options.path || "/"}`);
if (typeof options.maxAge === "number") {
parts.push(`Max-Age=${Math.max(0, Math.floor(options.maxAge))}`);
}
if (options.httpOnly !== false) {
parts.push("HttpOnly");
}
parts.push(`SameSite=${options.sameSite || "Lax"}`);
if (options.secure) {
parts.push("Secure");
}
return parts.join("; ");
}
function shouldUseSecureCookies() {
return process.env.NODE_ENV === "production" || process.env.COOKIE_SECURE === "true";
}
function getExpectedOrigin(req) {
const forwardedProto = String(req.headers["x-forwarded-proto"] || "")
.split(",")[0]
.trim()
.toLowerCase();
const protocol = forwardedProto || (req.socket.encrypted ? "https" : "http");
const host = String(req.headers.host || "").trim();
return host ? `${protocol}://${host}` : "";
}
function matchesTrustedOrigin(req) {
const expectedOrigin = getExpectedOrigin(req);
if (!expectedOrigin) {
return false;
}
const origin = String(req.headers.origin || "").trim();
if (origin) {
return origin === expectedOrigin;
}
const referer = String(req.headers.referer || "").trim();
if (referer) {
try {
return new URL(referer).origin === expectedOrigin;
} catch (error) {
return false;
}
}
const fetchSite = String(req.headers["sec-fetch-site"] || "")
.trim()
.toLowerCase();
if (fetchSite) {
return fetchSite === "same-origin" || fetchSite === "same-site" || fetchSite === "none";
}
return false;
}
function redirect(res, location) {
res.writeHead(302, {
...SECURITY_HEADERS,
Location: location,
});
res.end();
}
function sendHtml(res, statusCode, html, extraHeaders = {}) {
res.writeHead(statusCode, {
...SECURITY_HEADERS,
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store",
...extraHeaders,
});
res.end(html);
}
function sendJson(res, statusCode, payload, extraHeaders = {}) {
res.writeHead(statusCode, {
...SECURITY_HEADERS,
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
...extraHeaders,
});
res.end(JSON.stringify(payload));
}
function sendNotFound(res) {
sendJson(res, 404, { error: "Not found" });
}
function clearSessionCookie(res) {
res.setHeader(
"Set-Cookie",
buildCookie(SESSION_COOKIE_NAME, "", {
maxAge: 0,
secure: shouldUseSecureCookies(),
}),
);
}
function setSessionCookie(res, value) {
res.setHeader(
"Set-Cookie",
buildCookie(SESSION_COOKIE_NAME, value, {
maxAge: Math.floor(sessionStore.sessionTtlMs / 1000),
secure: shouldUseSecureCookies(),
}),
);
}
async function readBody(req, limit = REQUEST_BODY_LIMIT) {
return new Promise((resolve, reject) => {
const chunks = [];
let size = 0;
req.on("data", (chunk) => {
size += chunk.length;
if (size > limit) {
reject(new Error("Request body too large."));
req.destroy();
return;
}
chunks.push(chunk);
});
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
req.on("error", reject);
});
}
function parseFormBody(rawBody) {
return Object.fromEntries(new URLSearchParams(rawBody));
}
function parseJsonBody(rawBody) {
try {
return JSON.parse(rawBody || "{}");
} catch (error) {
throw new HfApiError(400, "That request body was not valid JSON.");
}
}
async function getStaticAsset(fileName) {
if (staticCache.has(fileName)) {
return staticCache.get(fileName);
}
const filePath = path.join(STATIC_DIR, fileName);
const content = await fs.readFile(filePath);
staticCache.set(fileName, content);
return content;
}
async function sendStaticAsset(reqPath, res) {
const fileName = reqPath.replace(/^\/+/, "");
const extension = path.extname(fileName);
const contentType = mimeTypes[extension];
if (!contentType) {
sendNotFound(res);
return;
}
try {
const asset = await getStaticAsset(fileName);
res.writeHead(200, {
...SECURITY_HEADERS,
"Content-Type": contentType,
"Cache-Control": "public, max-age=300",
});
res.end(asset);
} catch (error) {
sendNotFound(res);
}
}
async function getRequestSession(req) {
const cookies = parseCookies(req.headers.cookie);
const cookieValue = cookies[SESSION_COOKIE_NAME];
if (!cookieValue) {
return null;
}
return sessionStore.validateSession(cookieValue);
}
async function getSessionAccessToken(session) {
if (!session) {
return null;
}
return sessionStore.getAccessToken(session.id, session.secret);
}
function getSessionCsrfToken(session) {
return sessionStore.getCsrfToken(session.id, session.secret);
}
function matchesCsrfToken(req, session) {
const token = req.headers["x-csrf-token"];
if (!token) {
return false;
}
return token === getSessionCsrfToken(session);
}
function sanitizeError(error) {
if (error instanceof HfApiError) {
return {
statusCode: error.statusCode,
message: error.publicMessage,
};
}
return {
statusCode: 500,
message: "Something went wrong while handling that request.",
};
}
function isAppRoute(pathname) {
if (pathname.startsWith("/api/") || pathname.startsWith("/public/") || pathname.startsWith("/auth/")) {
return false;
}
return true;
}
async function renderShell(req, res) {
const session = await getRequestSession(req);
if (!session) {
sendHtml(res, 200, renderLoginPage({}));
return;
}
sendHtml(
res,
200,
renderAppShell({
viewer: session.user,
csrfToken: getSessionCsrfToken(session),
}),
);
}
async function handleAuthFailure(res, session) {
if (session) {
await sessionStore.revokeSession(session.id);
}
clearSessionCookie(res);
sendJson(res, 401, { error: "Authentication required." });
}
async function handleLogin(req, res) {
if (!matchesTrustedOrigin(req)) {
sendHtml(
res,
403,
renderLoginPage({
loginError: "That sign-in attempt was blocked because the request origin did not match this workspace.",
}),
);
return;
}
const rawBody = await readBody(req);
const form = parseFormBody(rawBody);
const accessToken = (form.accessToken || "").trim();
if (!accessToken) {
sendHtml(
res,
400,
renderLoginPage({
loginError: "Enter a Hugging Face access token.",
}),
);
return;
}
try {
const viewer = await getViewer(accessToken);
const currentSession = await getRequestSession(req);
if (currentSession) {
await sessionStore.revokeSession(currentSession.id);
}
const session = await sessionStore.createSession({
accessToken,
user: viewer,
});
setSessionCookie(res, session.cookieValue);
redirect(res, "/");
} catch (error) {
const safeError = sanitizeError(error);
sendHtml(
res,
safeError.statusCode,
renderLoginPage({
loginError: safeError.message,
}),
);
}
}
async function handleLogout(req, res) {
const session = await getRequestSession(req);
if (session && !matchesCsrfToken(req, session)) {
sendJson(res, 403, { error: "That action was blocked because the security token did not match." });
return;
}
if (session) {
await sessionStore.revokeSession(session.id);
}
clearSessionCookie(res);
sendJson(res, 200, { ok: true });
}
async function handlePageApi(req, res, url) {
const session = await getRequestSession(req);
if (!session) {
sendJson(res, 401, { error: "Authentication required." });
return;
}
const accessToken = await getSessionAccessToken(session);
if (!accessToken) {
await handleAuthFailure(res, session);
return;
}
try {
const requestedPath = url.searchParams.get("path") || "/";
const payload = await getPageData(accessToken, requestedPath);
if (payload.viewer.username !== session.user.username || payload.viewer.tokenRole !== session.user.tokenRole) {
await sessionStore.updateUserSummary(session.id, payload.viewer);
}
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
if (safeError.statusCode === 401) {
await handleAuthFailure(res, session);
return;
}
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleSearchApi(req, res, url) {
const session = await getRequestSession(req);
if (!session) {
sendJson(res, 401, { error: "Authentication required." });
return;
}
const accessToken = await getSessionAccessToken(session);
if (!accessToken) {
await handleAuthFailure(res, session);
return;
}
try {
const query = url.searchParams.get("q") || "";
const payload = await getSearchResults(accessToken, url.searchParams.get("path") || "/", query);
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleFileDownload(req, res, url) {
const session = await getRequestSession(req);
if (!session) {
sendJson(res, 401, { error: "Authentication required." });
return;
}
const accessToken = await getSessionAccessToken(session);
if (!accessToken) {
await handleAuthFailure(res, session);
return;
}
try {
const type = url.searchParams.get("type");
const repoId = url.searchParams.get("repoId");
const filePath = url.searchParams.get("path");
const branch = url.searchParams.get("branch") || "";
if (!type || !repoId || !filePath) {
throw new HfApiError(400, "Missing file download parameters.");
}
const { blob, fileName, contentType } = await getDownloadBlob(accessToken, {
type,
repoId,
path: filePath,
branch,
});
const arrayBuffer = await blob.arrayBuffer();
res.writeHead(200, {
...SECURITY_HEADERS,
"Content-Type": contentType,
"Cache-Control": "no-store",
"Content-Disposition": `inline; filename="${fileName.replaceAll('"', "")}"`,
});
res.end(Buffer.from(arrayBuffer));
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function requireMutationSession(req, res) {
const session = await getRequestSession(req);
if (!session) {
sendJson(res, 401, { error: "Authentication required." });
return null;
}
if (!matchesCsrfToken(req, session)) {
sendJson(res, 403, { error: "That action was blocked because the security token did not match." });
return null;
}
const accessToken = await getSessionAccessToken(session);
if (!accessToken) {
await handleAuthFailure(res, session);
return null;
}
return { session, accessToken };
}
async function handleFileUpdate(req, res) {
const auth = await requireMutationSession(req, res);
if (!auth) {
return;
}
try {
const body = parseJsonBody(await readBody(req));
const payload = await updateTextFile(auth.accessToken, {
type: body.type,
repoId: body.repoId,
path: body.path,
branch: body.branch,
parentCommit: body.parentCommit,
content: body.content,
});
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleFileDelete(req, res) {
const auth = await requireMutationSession(req, res);
if (!auth) {
return;
}
try {
const body = parseJsonBody(await readBody(req));
const payload = await removeFile(auth.accessToken, {
type: body.type,
repoId: body.repoId,
path: body.path,
branch: body.branch,
parentCommit: body.parentCommit,
});
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleResourceCreate(req, res) {
const auth = await requireMutationSession(req, res);
if (!auth) {
return;
}
try {
const body = parseJsonBody(await readBody(req));
const payload = await createResource(auth.accessToken, {
type: body.type,
namespace: body.namespace,
name: body.name,
visibility: body.visibility,
sdk: body.sdk,
});
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleResourceMove(req, res) {
const auth = await requireMutationSession(req, res);
if (!auth) {
return;
}
try {
const body = parseJsonBody(await readBody(req));
const payload = await moveResource(auth.accessToken, {
type: body.type,
fromId: body.fromId,
toId: body.toId,
});
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleResourceVisibility(req, res) {
const auth = await requireMutationSession(req, res);
if (!auth) {
return;
}
try {
const body = parseJsonBody(await readBody(req));
const payload = await updateResourceVisibility(auth.accessToken, {
type: body.type,
repoId: body.repoId,
visibility: body.visibility,
private: body.private,
});
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleResourceDelete(req, res) {
const auth = await requireMutationSession(req, res);
if (!auth) {
return;
}
try {
const body = parseJsonBody(await readBody(req));
const payload = await deleteResource(auth.accessToken, {
type: body.type,
repoId: body.repoId,
});
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleSpaceSecretSave(req, res, owner, name) {
const auth = await requireMutationSession(req, res);
if (!auth) {
return;
}
try {
const body = parseJsonBody(await readBody(req));
const payload = await saveSpaceSecret(auth.accessToken, `${owner}/${name}`, body);
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleSpaceVariableSave(req, res, owner, name) {
const auth = await requireMutationSession(req, res);
if (!auth) {
return;
}
try {
const body = parseJsonBody(await readBody(req));
const payload = await saveSpaceVariable(auth.accessToken, `${owner}/${name}`, body);
sendJson(res, 200, payload);
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
async function handleSpaceAction(req, res, owner, name) {
const auth = await requireMutationSession(req, res);
if (!auth) {
return;
}
try {
const body = parseJsonBody(await readBody(req));
const payload = await performSpaceAction(auth.accessToken, `${owner}/${name}`, body.action);
sendJson(res, 200, {
ok: true,
action: body.action,
payload,
});
} catch (error) {
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode, { error: safeError.message });
}
}
function sendSseHeaders(res) {
res.writeHead(200, {
...SECURITY_HEADERS,
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-store",
Connection: "keep-alive",
});
}
function writeSseEvent(res, eventName, data) {
if (eventName) {
res.write(`event: ${eventName}\n`);
}
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
async function proxySpaceStream(req, res, owner, name, kind) {
const session = await getRequestSession(req);
if (!session) {
sendJson(res, 401, { error: "Authentication required." });
return;
}
const accessToken = await getSessionAccessToken(session);
if (!accessToken) {
await handleAuthFailure(res, session);
return;
}
const streamKind =
kind === "events" || kind === "metrics" || kind === "build" || kind === "run" ? kind : "run";
const upstreamUrl = buildSpaceStreamUrl(`${owner}/${name}`, streamKind);
const abortController = new AbortController();
req.on("close", () => abortController.abort());
try {
const upstream = await fetch(upstreamUrl, {
signal: abortController.signal,
headers: {
Accept: "text/event-stream",
Authorization: `Bearer ${accessToken}`,
},
});
if (!upstream.ok || !upstream.body) {
const safeError = sanitizeError(new HfApiError(upstream.status, "Couldn't open that Space stream."));
sendSseHeaders(res);
writeSseEvent(res, "error", { error: safeError.message });
res.end();
return;
}
sendSseHeaders(res);
for await (const chunk of upstream.body) {
res.write(Buffer.from(chunk));
}
} catch (error) {
if (!abortController.signal.aborted) {
sendSseHeaders(res);
writeSseEvent(res, "error", { error: "The live Space stream disconnected." });
}
} finally {
res.end();
}
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function streamResourceUpdates(req, res, type, owner, name, url) {
const session = await getRequestSession(req);
if (!session) {
sendJson(res, 401, { error: "Authentication required." });
return;
}
const accessToken = await getSessionAccessToken(session);
if (!accessToken) {
await handleAuthFailure(res, session);
return;
}
sendSseHeaders(res);
let live = true;
let lastSignature = "";
req.on("close", () => {
live = false;
});
while (live) {
try {
const snapshot = await getResourceUpdates(accessToken, type, `${owner}/${name}`, {
branch: url.searchParams.get("branch") || "",
path: url.searchParams.get("path") || "",
});
if (snapshot.signature !== lastSignature) {
lastSignature = snapshot.signature;
writeSseEvent(res, "update", snapshot);
} else {
res.write(": keepalive\n\n");
}
} catch (error) {
const safeError = sanitizeError(error);
writeSseEvent(res, "error", { error: safeError.message });
}
await delay(15000);
}
}
async function handleRequest(req, res) {
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
const pathname = url.pathname;
if (req.method === "GET" && pathname === "/health") {
sendJson(res, 200, { status: "ok" });
return;
}
if (req.method === "POST" && pathname === "/auth/login") {
await handleLogin(req, res);
return;
}
if (req.method === "POST" && pathname === "/auth/logout") {
await handleLogout(req, res);
return;
}
if (req.method === "GET" && pathname === "/api/page") {
await handlePageApi(req, res, url);
return;
}
if (req.method === "GET" && pathname === "/api/search") {
await handleSearchApi(req, res, url);
return;
}
if (req.method === "GET" && pathname === "/api/file/download") {
await handleFileDownload(req, res, url);
return;
}
if (req.method === "POST" && pathname === "/api/files/update") {
await handleFileUpdate(req, res);
return;
}
if (req.method === "POST" && pathname === "/api/files/delete") {
await handleFileDelete(req, res);
return;
}
if (req.method === "POST" && pathname === "/api/resources/create") {
await handleResourceCreate(req, res);
return;
}
if (req.method === "POST" && pathname === "/api/resources/move") {
await handleResourceMove(req, res);
return;
}
if (req.method === "POST" && pathname === "/api/resources/visibility") {
await handleResourceVisibility(req, res);
return;
}
if (req.method === "POST" && pathname === "/api/resources/delete") {
await handleResourceDelete(req, res);
return;
}
const spaceSecretMatch =
req.method === "POST" ? pathname.match(/^\/api\/spaces\/([^/]+)\/([^/]+)\/secrets$/) : null;
if (spaceSecretMatch) {
await handleSpaceSecretSave(req, res, decodeURIComponent(spaceSecretMatch[1]), decodeURIComponent(spaceSecretMatch[2]));
return;
}
const spaceVariableMatch =
req.method === "POST" ? pathname.match(/^\/api\/spaces\/([^/]+)\/([^/]+)\/variables$/) : null;
if (spaceVariableMatch) {
await handleSpaceVariableSave(req, res, decodeURIComponent(spaceVariableMatch[1]), decodeURIComponent(spaceVariableMatch[2]));
return;
}
const spaceActionMatch =
req.method === "POST" ? pathname.match(/^\/api\/spaces\/([^/]+)\/([^/]+)\/action$/) : null;
if (spaceActionMatch) {
await handleSpaceAction(req, res, decodeURIComponent(spaceActionMatch[1]), decodeURIComponent(spaceActionMatch[2]));
return;
}
const spaceLogsMatch =
req.method === "GET" ? pathname.match(/^\/api\/spaces\/([^/]+)\/([^/]+)\/logs\/stream$/) : null;
if (spaceLogsMatch) {
const kind = url.searchParams.get("kind") === "build" ? "build" : "run";
await proxySpaceStream(req, res, decodeURIComponent(spaceLogsMatch[1]), decodeURIComponent(spaceLogsMatch[2]), kind);
return;
}
const spaceEventsMatch =
req.method === "GET" ? pathname.match(/^\/api\/spaces\/([^/]+)\/([^/]+)\/events\/stream$/) : null;
if (spaceEventsMatch) {
await proxySpaceStream(req, res, decodeURIComponent(spaceEventsMatch[1]), decodeURIComponent(spaceEventsMatch[2]), "events");
return;
}
const spaceMetricsMatch =
req.method === "GET" ? pathname.match(/^\/api\/spaces\/([^/]+)\/([^/]+)\/metrics\/stream$/) : null;
if (spaceMetricsMatch) {
await proxySpaceStream(req, res, decodeURIComponent(spaceMetricsMatch[1]), decodeURIComponent(spaceMetricsMatch[2]), "metrics");
return;
}
const updatesMatch =
req.method === "GET"
? pathname.match(/^\/api\/resources\/(space|model|dataset|bucket)\/([^/]+)\/([^/]+)\/updates\/stream$/)
: null;
if (updatesMatch) {
await streamResourceUpdates(
req,
res,
decodeURIComponent(updatesMatch[1]),
decodeURIComponent(updatesMatch[2]),
decodeURIComponent(updatesMatch[3]),
url,
);
return;
}
if (req.method === "GET" && pathname.startsWith("/public/")) {
await sendStaticAsset(pathname.replace("/public/", ""), res);
return;
}
if (req.method === "GET" && isAppRoute(pathname)) {
await renderShell(req, res);
return;
}
sendNotFound(res);
}
async function main() {
const server = await startServer({ port: PORT, host: HOST });
console.log(`HF Home listening on http://${HOST}:${PORT}`);
return server;
}
async function startServer({ port, host }) {
await sessionStore.init();
const server = http.createServer(async (req, res) => {
try {
await handleRequest(req, res);
} catch (error) {
console.error(error);
const safeError = sanitizeError(error);
sendJson(res, safeError.statusCode || 500, { error: safeError.message });
}
});
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, host, () => {
server.off("error", reject);
resolve(server);
});
});
}
if (require.main === module) {
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
}
module.exports = {
startServer,
};