Spaces:
Sleeping
Sleeping
| import { config, ready } from "$lib/server/config"; | |
| import type { Handle, HandleServerError, ServerInit, HandleFetch } from "@sveltejs/kit"; | |
| import { collections } from "$lib/server/database"; | |
| import { base } from "$app/paths"; | |
| import { | |
| authenticateRequest, | |
| loginEnabled, | |
| refreshSessionCookie, | |
| triggerOauthFlow, | |
| } from "$lib/server/auth"; | |
| import { ERROR_MESSAGES } from "$lib/stores/errors"; | |
| import { addWeeks } from "date-fns"; | |
| import { checkAndRunMigrations } from "$lib/migrations/migrations"; | |
| import { building, dev } from "$app/environment"; | |
| import { logger } from "$lib/server/logger"; | |
| import { AbortedGenerations } from "$lib/server/abortedGenerations"; | |
| import { initExitHandler } from "$lib/server/exitHandler"; | |
| import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats"; | |
| import { adminTokenManager } from "$lib/server/adminToken"; | |
| import { isHostLocalhost } from "$lib/server/isURLLocal"; | |
| import { MetricsServer } from "$lib/server/metrics"; | |
| export const init: ServerInit = async () => { | |
| // Wait for config to be fully loaded | |
| await ready; | |
| // TODO: move this code on a started server hook, instead of using a "building" flag | |
| if (!building) { | |
| // Ensure legacy env expected by some libs: map OPENAI_API_KEY -> HF_TOKEN if absent | |
| const canonicalToken = config.OPENAI_API_KEY || config.HF_TOKEN; | |
| if (canonicalToken) { | |
| process.env.HF_TOKEN ??= canonicalToken; | |
| } | |
| // Warn if legacy-only var is used | |
| if (!config.OPENAI_API_KEY && config.HF_TOKEN) { | |
| logger.warn( | |
| "HF_TOKEN is deprecated in favor of OPENAI_API_KEY. Please migrate to OPENAI_API_KEY." | |
| ); | |
| } | |
| logger.info("Starting server..."); | |
| initExitHandler(); | |
| if (config.METRICS_ENABLED === "true") { | |
| MetricsServer.getInstance(); | |
| } | |
| checkAndRunMigrations(); | |
| refreshConversationStats(); | |
| // Init AbortedGenerations refresh process | |
| AbortedGenerations.getInstance(); | |
| adminTokenManager.displayToken(); | |
| if (config.EXPOSE_API) { | |
| logger.warn( | |
| "The EXPOSE_API flag has been deprecated. The API is now required for chat-ui to work." | |
| ); | |
| } | |
| } | |
| }; | |
| export const handleError: HandleServerError = async ({ error, event, status, message }) => { | |
| // handle 404 | |
| if (building) { | |
| throw error; | |
| } | |
| if (event.route.id === null) { | |
| return { | |
| message: `Page ${event.url.pathname} not found`, | |
| }; | |
| } | |
| const errorId = crypto.randomUUID(); | |
| logger.error({ | |
| locals: event.locals, | |
| url: event.request.url, | |
| params: event.params, | |
| request: event.request, | |
| message, | |
| error, | |
| errorId, | |
| status, | |
| stack: error instanceof Error ? error.stack : undefined, | |
| }); | |
| return { | |
| message: "An error occurred", | |
| errorId, | |
| }; | |
| }; | |
| export const handle: Handle = async ({ event, resolve }) => { | |
| await ready.then(() => { | |
| config.checkForUpdates(); | |
| }); | |
| logger.debug({ | |
| locals: event.locals, | |
| url: event.url.pathname, | |
| params: event.params, | |
| request: event.request, | |
| }); | |
| function errorResponse(status: number, message: string) { | |
| const sendJson = | |
| event.request.headers.get("accept")?.includes("application/json") || | |
| event.request.headers.get("content-type")?.includes("application/json"); | |
| return new Response(sendJson ? JSON.stringify({ error: message }) : message, { | |
| status, | |
| headers: { | |
| "content-type": sendJson ? "application/json" : "text/plain", | |
| }, | |
| }); | |
| } | |
| if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) { | |
| const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET; | |
| if (!ADMIN_SECRET) { | |
| return errorResponse(500, "Admin API is not configured"); | |
| } | |
| if (event.request.headers.get("Authorization") !== `Bearer ${ADMIN_SECRET}`) { | |
| return errorResponse(401, "Unauthorized"); | |
| } | |
| } | |
| const auth = await authenticateRequest( | |
| { type: "svelte", value: event.request.headers }, | |
| { type: "svelte", value: event.cookies }, | |
| event.url | |
| ); | |
| event.locals.sessionId = auth.sessionId; | |
| if (loginEnabled && !auth.user && !event.url.pathname.startsWith(`${base}/.well-known/`)) { | |
| if (config.AUTOMATIC_LOGIN === "true") { | |
| // AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages) | |
| if ( | |
| !event.url.pathname.startsWith(`${base}/login`) && | |
| !event.url.pathname.startsWith(`${base}/healthcheck`) | |
| ) { | |
| // To get the same CSRF token after callback | |
| refreshSessionCookie(event.cookies, auth.secretSessionId); | |
| return await triggerOauthFlow(event); | |
| } | |
| } else { | |
| // Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails) | |
| if ( | |
| event.url.pathname !== `${base}/` && | |
| event.url.pathname !== `${base}` && | |
| !event.url.pathname.startsWith(`${base}/login`) && | |
| !event.url.pathname.startsWith(`${base}/login/callback`) && | |
| !event.url.pathname.startsWith(`${base}/healthcheck`) && | |
| !event.url.pathname.startsWith(`${base}/r/`) && | |
| !event.url.pathname.startsWith(`${base}/conversation/`) && | |
| !event.url.pathname.startsWith(`${base}/models/`) && | |
| !event.url.pathname.startsWith(`${base}/api`) | |
| ) { | |
| refreshSessionCookie(event.cookies, auth.secretSessionId); | |
| return triggerOauthFlow(event); | |
| } | |
| } | |
| } | |
| event.locals.user = auth.user || undefined; | |
| event.locals.token = auth.token; | |
| event.locals.isAdmin = | |
| event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId); | |
| // CSRF protection | |
| const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? ""; | |
| /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */ | |
| const nativeFormContentTypes = [ | |
| "multipart/form-data", | |
| "application/x-www-form-urlencoded", | |
| "text/plain", | |
| ]; | |
| if (event.request.method === "POST") { | |
| if (nativeFormContentTypes.includes(requestContentType)) { | |
| const origin = event.request.headers.get("origin"); | |
| if (!origin) { | |
| return errorResponse(403, "Non-JSON form requests need to have an origin"); | |
| } | |
| const validOrigins = [ | |
| new URL(event.request.url).host, | |
| ...(config.PUBLIC_ORIGIN ? [new URL(config.PUBLIC_ORIGIN).host] : []), | |
| ]; | |
| if (!validOrigins.includes(new URL(origin).host)) { | |
| return errorResponse(403, "Invalid referer for POST request"); | |
| } | |
| } | |
| } | |
| if ( | |
| event.request.method === "POST" || | |
| event.url.pathname.startsWith(`${base}/login`) || | |
| event.url.pathname.startsWith(`${base}/login/callback`) | |
| ) { | |
| // if the request is a POST request or login-related we refresh the cookie | |
| refreshSessionCookie(event.cookies, auth.secretSessionId); | |
| await collections.sessions.updateOne( | |
| { sessionId: auth.sessionId }, | |
| { $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } } | |
| ); | |
| } | |
| if ( | |
| loginEnabled && | |
| !event.locals.user && | |
| !event.url.pathname.startsWith(`${base}/login`) && | |
| !event.url.pathname.startsWith(`${base}/admin`) && | |
| !event.url.pathname.startsWith(`${base}/settings`) && | |
| !["GET", "OPTIONS", "HEAD"].includes(event.request.method) | |
| ) { | |
| return errorResponse(401, ERROR_MESSAGES.authOnly); | |
| } | |
| let replaced = false; | |
| const response = await resolve(event, { | |
| transformPageChunk: (chunk) => { | |
| // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template | |
| if (replaced || !chunk.html.includes("%gaId%")) { | |
| return chunk.html; | |
| } | |
| replaced = true; | |
| return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID); | |
| }, | |
| filterSerializedResponseHeaders: (header) => { | |
| return header.includes("content-type"); | |
| }, | |
| }); | |
| // Add CSP header to disallow framing if ALLOW_IFRAME is not "true" | |
| if (config.ALLOW_IFRAME !== "true") { | |
| response.headers.append("Content-Security-Policy", "frame-ancestors 'none';"); | |
| } | |
| if ( | |
| event.url.pathname.startsWith(`${base}/login/callback`) || | |
| event.url.pathname.startsWith(`${base}/login`) | |
| ) { | |
| response.headers.append("Cache-Control", "no-store"); | |
| } | |
| if (event.url.pathname.startsWith(`${base}/api/`)) { | |
| // get origin from the request | |
| const requestOrigin = event.request.headers.get("origin"); | |
| // get origin from the config if its defined | |
| let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined; | |
| if ( | |
| dev || // if we're in dev mode | |
| !requestOrigin || // or the origin is null (SSR) | |
| isHostLocalhost(new URL(requestOrigin).hostname) // or the origin is localhost | |
| ) { | |
| allowedOrigin = "*"; // allow all origins | |
| } else if (allowedOrigin === requestOrigin) { | |
| allowedOrigin = requestOrigin; // echo back the caller | |
| } | |
| if (allowedOrigin) { | |
| response.headers.set("Access-Control-Allow-Origin", allowedOrigin); | |
| response.headers.set( | |
| "Access-Control-Allow-Methods", | |
| "GET, POST, PUT, PATCH, DELETE, OPTIONS" | |
| ); | |
| response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); | |
| } | |
| } | |
| return response; | |
| }; | |
| export const handleFetch: HandleFetch = async ({ event, request, fetch }) => { | |
| if (isHostLocalhost(new URL(request.url).hostname)) { | |
| const cookieHeader = event.request.headers.get("cookie"); | |
| if (cookieHeader) { | |
| const headers = new Headers(request.headers); | |
| headers.set("cookie", cookieHeader); | |
| return fetch(new Request(request, { headers })); | |
| } | |
| } | |
| return fetch(request); | |
| }; | |