import { z } from "zod"; import dotenv from "dotenv"; console.log(dotenv.config({ path: ".env.local", debug: true })); const stringBool = (defaultValue: string) => z .string() .default(defaultValue) .refine((s) => s === "true" || s === "false") .transform((s) => s === "true"); const allEnv = z.object({ API_URL: z.string().url().default("http://localhost:3000"), DISABLE_SIGNUPS: stringBool("false"), DISABLE_PASSWORD_AUTH: stringBool("false"), OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING: stringBool("false"), OAUTH_WELLKNOWN_URL: z.string().url().optional(), OAUTH_CLIENT_SECRET: z.string().optional(), OAUTH_CLIENT_ID: z.string().optional(), OAUTH_SCOPE: z.string().default("openid email profile"), OAUTH_PROVIDER_NAME: z.string().default("Custom Provider"), OPENAI_API_KEY: z.string().optional(), OPENAI_BASE_URL: z.string().url().optional(), OLLAMA_BASE_URL: z.string().url().optional(), OLLAMA_KEEP_ALIVE: z.string().optional(), INFERENCE_JOB_TIMEOUT_SEC: z.coerce.number().default(30), INFERENCE_TEXT_MODEL: z.string().default("gpt-4o-mini"), INFERENCE_IMAGE_MODEL: z.string().default("gpt-4o-mini"), INFERENCE_CONTEXT_LENGTH: z.coerce.number().default(2048), OCR_CACHE_DIR: z.string().optional(), OCR_LANGS: z .string() .default("eng") .transform((val) => val.split(",")), OCR_CONFIDENCE_THRESHOLD: z.coerce.number().default(50), CRAWLER_HEADLESS_BROWSER: stringBool("true"), BROWSER_WEB_URL: z.string().url().optional(), BROWSER_WEBSOCKET_URL: z.string().url().optional(), BROWSER_CONNECT_ONDEMAND: stringBool("false"), CRAWLER_JOB_TIMEOUT_SEC: z.coerce.number().default(60), CRAWLER_NAVIGATE_TIMEOUT_SEC: z.coerce.number().default(30), CRAWLER_NUM_WORKERS: z.coerce.number().default(1), CRAWLER_DOWNLOAD_BANNER_IMAGE: stringBool("true"), CRAWLER_STORE_SCREENSHOT: stringBool("true"), CRAWLER_FULL_PAGE_SCREENSHOT: stringBool("false"), CRAWLER_FULL_PAGE_ARCHIVE: stringBool("false"), CRAWLER_VIDEO_DOWNLOAD: stringBool("false"), CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE: z.coerce.number().default(50), CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC: z.coerce.number().default(10 * 60), MEILI_ADDR: z.string().optional(), MEILI_MASTER_KEY: z.string().default(""), LOG_LEVEL: z.string().default("debug"), DEMO_MODE: stringBool("false"), DEMO_MODE_EMAIL: z.string().optional(), DEMO_MODE_PASSWORD: z.string().optional(), DATA_DIR: z.string().default(""), MAX_ASSET_SIZE_MB: z.coerce.number().default(4), INFERENCE_LANG: z.string().default("english"), // Build only flag SERVER_VERSION: z.string().optional(), DISABLE_NEW_RELEASE_CHECK: stringBool("false"), // A flag to detect if the user is running in the old separete containers setup USING_LEGACY_SEPARATE_CONTAINERS: stringBool("false"), }); const serverConfigSchema = allEnv.transform((val) => { return { apiUrl: val.API_URL, auth: { disableSignups: val.DISABLE_SIGNUPS, disablePasswordAuth: val.DISABLE_PASSWORD_AUTH, oauth: { allowDangerousEmailAccountLinking: val.OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING, wellKnownUrl: val.OAUTH_WELLKNOWN_URL, clientSecret: val.OAUTH_CLIENT_SECRET, clientId: val.OAUTH_CLIENT_ID, scope: val.OAUTH_SCOPE, name: val.OAUTH_PROVIDER_NAME, }, }, inference: { jobTimeoutSec: val.INFERENCE_JOB_TIMEOUT_SEC, openAIApiKey: val.OPENAI_API_KEY, openAIBaseUrl: val.OPENAI_BASE_URL, ollamaBaseUrl: val.OLLAMA_BASE_URL, ollamaKeepAlive: val.OLLAMA_KEEP_ALIVE, textModel: val.INFERENCE_TEXT_MODEL, imageModel: val.INFERENCE_IMAGE_MODEL, inferredTagLang: val.INFERENCE_LANG, contextLength: val.INFERENCE_CONTEXT_LENGTH, }, crawler: { numWorkers: val.CRAWLER_NUM_WORKERS, headlessBrowser: val.CRAWLER_HEADLESS_BROWSER, browserWebUrl: val.BROWSER_WEB_URL, browserWebSocketUrl: val.BROWSER_WEBSOCKET_URL, browserConnectOnDemand: val.BROWSER_CONNECT_ONDEMAND, jobTimeoutSec: val.CRAWLER_JOB_TIMEOUT_SEC, navigateTimeoutSec: val.CRAWLER_NAVIGATE_TIMEOUT_SEC, downloadBannerImage: val.CRAWLER_DOWNLOAD_BANNER_IMAGE, storeScreenshot: val.CRAWLER_STORE_SCREENSHOT, fullPageScreenshot: val.CRAWLER_FULL_PAGE_SCREENSHOT, fullPageArchive: val.CRAWLER_FULL_PAGE_ARCHIVE, downloadVideo: val.CRAWLER_VIDEO_DOWNLOAD, maxVideoDownloadSize: val.CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE, downloadVideoTimeout: val.CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC, }, ocr: { langs: val.OCR_LANGS, cacheDir: val.OCR_CACHE_DIR, confidenceThreshold: val.OCR_CONFIDENCE_THRESHOLD, }, meilisearch: val.MEILI_ADDR ? { address: val.MEILI_ADDR, key: val.MEILI_MASTER_KEY, } : undefined, logLevel: val.LOG_LEVEL, demoMode: val.DEMO_MODE ? { email: val.DEMO_MODE_EMAIL, password: val.DEMO_MODE_PASSWORD, } : undefined, dataDir: val.DATA_DIR, maxAssetSizeMb: val.MAX_ASSET_SIZE_MB, serverVersion: val.SERVER_VERSION, disableNewReleaseCheck: val.DISABLE_NEW_RELEASE_CHECK, usingLegacySeparateContainers: val.USING_LEGACY_SEPARATE_CONTAINERS, }; }); const serverConfig = serverConfigSchema.parse(process.env); // Always explicitly pick up stuff from server config to avoid accidentally leaking stuff export const clientConfig = { demoMode: serverConfig.demoMode, auth: { disableSignups: serverConfig.auth.disableSignups, disablePasswordAuth: serverConfig.auth.disablePasswordAuth, }, inference: { inferredTagLang: serverConfig.inference.inferredTagLang, }, serverVersion: serverConfig.serverVersion, disableNewReleaseCheck: serverConfig.disableNewReleaseCheck, }; export type ClientConfig = typeof clientConfig; export default serverConfig;