import { z } from "zod"; import { FALLBACK_HOTEL_NAME } from "@/lib/brand"; // Minimal validated env for the foundation. When the Next.js app is added this // will move to @t3-oss/env-nextjs (the habbo-next pattern), but the data layer // only needs the DB connection + a couple of values today. const schema = z.object({ NODE_ENV: z .enum(["development", "test", "production"]) .default("development"), DATABASE_URL: z.string().url(), DATABASE_POOL_SIZE: z.coerce.number().int().positive().default(10), DATABASE_IDLE_TIMEOUT_MS: z.coerce.number().int().positive().default(300_000), DATABASE_CONNECT_TIMEOUT_MS: z.coerce .number() .int() .positive() .default(10_000), HOTEL_NAME: z.string().default(FALLBACK_HOTEL_NAME), APP_URL: z.string().url().default("http://localhost:3000"), // Resend API key (preferred — simpler HTTP API, always works). RESEND_API_KEY: z.string().optional(), // SMTP (password reset / notifications). Email features no-op if unset. SMTP_HOST: z.string().optional(), SMTP_PORT: z.coerce.number().int().positive().optional(), SMTP_SECURE: z .string() .optional() .transform((v) => v === "true" || v === "1"), SMTP_USER: z.string().optional(), SMTP_PASSWORD: z.string().optional(), SMTP_FROM: z.string().optional(), // RCON link to the Arcturus emulator (raw-JSON TCP protocol). RCON_HOST: z.string().default("127.0.0.1"), RCON_PORT: z.coerce.number().int().positive().default(3001), RCON_TIMEOUT_MS: z.coerce.number().int().positive().default(10_000), RCON_MAX_RETRIES: z.coerce.number().int().positive().default(3), // NextAuth v5 reads AUTH_SECRET itself; declared here for documentation/typing. AUTH_SECRET: z.string().min(1).optional(), // Laravel APP_KEY (base64:...) — needed to read existing 2FA secrets. APP_KEY: z.string().optional(), // Optional OAuth providers (enabled only when both id+secret are set). DISCORD_CLIENT_ID: z.string().optional(), DISCORD_CLIENT_SECRET: z.string().optional(), GOOGLE_CLIENT_ID: z.string().optional(), GOOGLE_CLIENT_SECRET: z.string().optional(), // Mirrors Laravel config('habbo.site.convert_passwords') — enables md5->argon2id. CONVERT_PASSWORDS: z .string() .optional() .transform((v) => v === "true" || v === "1"), // Hashing driver for NEW passwords: bcrypt (default, fits varchar(64)) | argon2id. PASSWORD_HASH: z.enum(["bcrypt", "argon2id"]).optional(), // Filesystem dir the badge uploader writes .gif into (the emulator's // badge image folder, e.g. .../assets/c_images/album1584). Upload is disabled // when unset. BADGE_UPLOAD_DIR: z.string().optional(), // Emulator JAR backup job (jobs-worker, host-side); no-op unless both set. EMULATOR_JAR_PATH: z.string().optional(), EMULATOR_BACKUP_DIR: z.string().optional(), EMULATOR_BACKUP_KEEP: z.coerce.number().int().positive().optional(), // Optional AI content moderation (comments / guestbook). OPENAI_API_KEY: z.string().optional(), // Optional alerting (jobs worker / alert service). DISCORD_WEBHOOK_URL: z.string().url().optional(), TELEGRAM_BOT_TOKEN: z.string().optional(), TELEGRAM_CHAT_ID: z.string().optional(), ALERT_EMAIL: z.string().optional(), // Optional PayPal top-up. PAYPAL_CLIENT_ID: z.string().optional(), PAYPAL_SECRET: z.string().optional(), PAYPAL_API: z.string().url().optional(), // Redis — strongly recommended in production (required for multi-instance). // Without it, rate limits / shared caches are in-process only. REDIS_URL: z.string().optional(), // Logging level. LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).optional(), // Optional Sentry — no-op when unset. SENTRY_DSN: z.string().url().optional(), NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(), SENTRY_ORG: z.string().optional(), SENTRY_PROJECT: z.string().optional(), SENTRY_AUTH_TOKEN: z.string().optional(), APP_VERSION: z.string().optional(), NEXT_PUBLIC_APP_VERSION: z.string().optional(), }).superRefine((data, ctx) => { if (data.NODE_ENV !== "production") return; if (!data.AUTH_SECRET || data.AUTH_SECRET.length < 32) { ctx.addIssue({ code: "custom", message: "AUTH_SECRET (>=32 characters) is required when NODE_ENV=production", path: ["AUTH_SECRET"], }); } }); type Env = z.infer; // SKIP_ENV_VALIDATION is for tooling only (vitest). Production deploy must NOT // set this — builds should validate AUTH_SECRET, DATABASE_URL, etc. export const env: Env = process.env.SKIP_ENV_VALIDATION ? (process.env as unknown as Env) : schema.parse(process.env);