Spaces:
Running
Running
| 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, | |
| }; | |