"use strict"; require("dotenv").config({ path: ".env.local", quiet: true }); require("dotenv").config({ path: ".env", quiet: true }); const { z } = require("zod"); function intFromEnv(name, fallback) { const raw = process.env[name]; if (!raw) { return fallback; } const parsed = Number.parseInt(raw, 10); return Number.isInteger(parsed) ? parsed : fallback; } function strFromEnv(name, fallback) { const raw = process.env[name]; return raw && String(raw).trim() ? String(raw).trim() : fallback; } function listFromEnv(name, fallback = []) { const raw = process.env[name]; if (!raw) { return fallback; } return String(raw) .split(",") .map((item) => item.trim()) .filter(Boolean); } function boolFromEnv(name, fallback) { const raw = process.env[name]; if (!raw) { return fallback; } const normalized = String(raw).trim().toLowerCase(); if (["1", "true", "yes", "on"].includes(normalized)) { return true; } if (["0", "false", "no", "off"].includes(normalized)) { return false; } return fallback; } const parsed = { nodeEnv: strFromEnv("NODE_ENV", "development"), port: intFromEnv("PORT", 3000), logLevel: strFromEnv("LOG_LEVEL", "info"), appBaseUrl: strFromEnv("APP_BASE_URL", "http://localhost:3000"), betterAuthSecret: strFromEnv("BETTER_AUTH_SECRET", "dev-better-auth-secret"), betterAuthBasePath: strFromEnv("BETTER_AUTH_BASE_PATH", "/api/auth"), xOAuthClientId: strFromEnv("X_OAUTH_CLIENT_ID", ""), xOAuthClientSecret: strFromEnv("X_OAUTH_CLIENT_SECRET", ""), internalApiToken: strFromEnv("INTERNAL_API_TOKEN", ""), convexDeploymentUrl: strFromEnv("CONVEX_DEPLOYMENT_URL", strFromEnv("CONVEX_URL", "")), convexAuthToken: strFromEnv("CONVEX_AUTH_TOKEN", ""), convexStateQuery: strFromEnv("CONVEX_STATE_QUERY", "state:getLatestSnapshot"), convexStateMutation: strFromEnv("CONVEX_STATE_MUTATION", "state:saveSnapshot"), xWebhookSecret: process.env.X_WEBHOOK_SECRET || "dev-x-secret", xBearerToken: strFromEnv("X_BEARER_TOKEN", ""), xBotUserId: strFromEnv("X_BOT_USER_ID", ""), polarWebhookSecret: process.env.POLAR_WEBHOOK_SECRET || "dev-polar-secret", polarAccessToken: strFromEnv("POLAR_ACCESS_TOKEN", ""), polarServer: strFromEnv("POLAR_SERVER", "production"), polarProductIds: listFromEnv("POLAR_PRODUCT_IDS", []), qwenTtsApiKey: strFromEnv("QWEN_TTS_API_KEY", ""), qwenTtsBaseUrl: strFromEnv("QWEN_TTS_BASE_URL", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"), qwenTtsModel: strFromEnv("QWEN_TTS_MODEL", "qwen-tts-latest"), qwenTtsVoice: strFromEnv("QWEN_TTS_VOICE", "Cherry"), qwenTtsFormat: strFromEnv("QWEN_TTS_FORMAT", "mp3"), minioEndPoint: strFromEnv("MINIO_ENDPOINT", ""), minioPort: intFromEnv("MINIO_PORT", 443), minioUseSSL: boolFromEnv("MINIO_USE_SSL", true), minioBucket: strFromEnv("MINIO_BUCKET", ""), minioRegion: strFromEnv("MINIO_REGION", "us-east-1"), minioAccessKey: strFromEnv("MINIO_ACCESS_KEY", ""), minioSecretKey: strFromEnv("MINIO_SECRET_KEY", ""), minioSignedUrlTtlSec: intFromEnv("MINIO_SIGNED_URL_TTL_SEC", 3600), rateLimits: { webhookPerMinute: intFromEnv("WEBHOOK_RPM", 120), authPerMinute: intFromEnv("AUTH_RPM", 30), actionPerMinute: intFromEnv("ACTION_RPM", 60), }, abuse: { maxJobsPerUserPerDay: intFromEnv("ABUSE_MAX_JOBS_PER_USER_PER_DAY", 0), cooldownSec: intFromEnv("ABUSE_COOLDOWN_SEC", 0), denyUserIds: listFromEnv("ABUSE_DENY_USER_IDS", []), }, credit: { baseCredits: intFromEnv("BASE_CREDITS", 1), includedChars: intFromEnv("INCLUDED_CHARS", 25000), stepChars: intFromEnv("STEP_CHARS", 10000), stepCredits: intFromEnv("STEP_CREDITS", 1), maxCharsPerArticle: intFromEnv("MAX_CHARS_PER_ARTICLE", 120000), }, }; parsed.allowInMemoryStateFallback = boolFromEnv( "ALLOW_IN_MEMORY_STATE_FALLBACK", parsed.nodeEnv !== "production", ); const ConfigSchema = z.object({ nodeEnv: z.string().min(1), port: z.number().int().positive(), logLevel: z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]), appBaseUrl: z.string().min(1), betterAuthSecret: z.string().min(1), betterAuthBasePath: z.string().min(1), xOAuthClientId: z.string(), xOAuthClientSecret: z.string(), internalApiToken: z.string(), convexDeploymentUrl: z.string(), convexAuthToken: z.string(), convexStateQuery: z.string().min(1), convexStateMutation: z.string().min(1), xWebhookSecret: z.string().min(1), xBearerToken: z.string(), xBotUserId: z.string(), polarWebhookSecret: z.string().min(1), polarAccessToken: z.string(), polarServer: z.enum(["production", "sandbox"]), polarProductIds: z.array(z.string().min(1)), qwenTtsApiKey: z.string(), qwenTtsBaseUrl: z.string().min(1), qwenTtsModel: z.string().min(1), qwenTtsVoice: z.string().min(1), qwenTtsFormat: z.string().min(1), minioEndPoint: z.string(), minioPort: z.number().int().positive(), minioUseSSL: z.boolean(), minioBucket: z.string(), minioRegion: z.string(), minioAccessKey: z.string(), minioSecretKey: z.string(), minioSignedUrlTtlSec: z.number().int().positive(), rateLimits: z.object({ webhookPerMinute: z.number().int().positive(), authPerMinute: z.number().int().positive(), actionPerMinute: z.number().int().positive(), }), abuse: z.object({ maxJobsPerUserPerDay: z.number().int().nonnegative(), cooldownSec: z.number().int().nonnegative(), denyUserIds: z.array(z.string().min(1)), }), credit: z.object({ baseCredits: z.number().int().positive(), includedChars: z.number().int().positive(), stepChars: z.number().int().positive(), stepCredits: z.number().int().positive(), maxCharsPerArticle: z.number().int().positive(), }), allowInMemoryStateFallback: z.boolean(), }); const config = ConfigSchema.parse(parsed); module.exports = { config, };