import { OBJECT_IMAGE_FORMATS } from "@constants/investigations.constants";
import { config as dotenvConfig } from "dotenv";
import { z } from "zod";
// Load environment variables from .env file, if available
dotenvConfig({ quiet: true });
/**
* Environment variables schema and validation.
* @category Config
*/
const envSchema = z.object({
/**
* HTTP server port
* @category Config
*/
PORT: z.coerce.number().default(3000),
/**
* Socket IO server port
* @category Config
*/
SOCKET_PORT: z.coerce.number().default(3001),
/**
* WebSocket server port (for Unity WebGL and other platforms)
* @category Config
*/
WS_PORT: z.coerce.number().default(3002),
/**
* Node.js environment (development, production, test)
* @category Config
*/
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
/**
* MongoDB connection URI - must be provided
* @category Config
*/
MONGO_URI: z.string().url(),
/**
* Redis connection URI - must be provided
* @category Config
*/
REDIS_URI: z.string().url(),
/**
* Redis database index
* @category Config
*/
REDIS_DB_INDEX: z.coerce.number().int().min(0).default(0),
/**
* Authentication gRPC service hostname
* @category Config
*/
AUTH_GRPC_HOST: z.string().default("localhost"),
/**
* Authentication gRPC service port
* @category Config
*/
AUTH_GRPC_PORT: z.coerce.number().default(50051),
/**
* OpenAI Key
* @category Config
*/
OPENAI_API_KEY: z.coerce.string(),
/**
* TogetherAI APR key (this is for future)
* @category Config
*/
TOGETHER_API_KEY: z.coerce.string().default(""),
/**
* Replicate Key
* @category Config
*/
REPLICATE_API_TOKEN: z.coerce.string(),
/**
* OpenAI LLM model
* @category Config
*/
OPENAI_MODEL: z.coerce.string().default("gpt-4o"),
/**
* OpenAI LLM model temperature
* @category Config
*/
OPENAI_MODEL_TEMPERATURE: z.coerce.number().default(0.7),
/**
* Assistant greeting message to the user
* @category Config
*/
ASSISTANT_GREETING_MESSAGE: z
.string()
.default("Hi! My name is Galileo. I am here to guide you in shaping fascinating investigations that align with your curriculum. Please tell me what subject or area of study you would like to begin exploring."),
/**
* Cache Configuration
* @category Config
*/
CACHE_DURATION_DEFAULT: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().min(1000))
.default("60000"), // 1 minute
/**
* Cache duration short
* @category Config
*/
CACHE_DURATION_SHORT: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().min(1000))
.default("30000"), // 30 seconds
/**
* Cache duration long
* @category Config
*/
CACHE_DURATION_LONG: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().min(1000))
.default("300000"), // 5 minutes
/**
* gRPC Configuration
* @category Config
*/
GRPC_KEEPALIVE_TIME_MS: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().min(1000))
.default("30000"), // 30 seconds
/**
* gRPC HTTP2 ping interval
* @category Config
*/
GRPC_HTTP2_PING_INTERVAL_MS: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().min(1000))
.default("300000"), // 5 minutes
/**
* Investigation inactivity timeout in milliseconds
* @category Config
*/
INVESTIGATION_INACTIVITY_TIMEOUT_MS: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().min(1000))
.default("120000"), // 2 minutes
JWT_SECRET: z.string().default("secret"),
/**
* CORS allowed domain (e.g., ".aaic.dev" to allow all subdomains)
* @category Config
*/
CORS_ALLOWED_DOMAIN: z.string().default(".aaic.dev"),
/**
* HashiCorp Vault Configuration
* @category Config
*/
VAULT_URL: z.string().url().default("http://127.0.0.1:8200"),
/**
* HashiCorp Vault authentication token
* @category Config
*/
VAULT_TOKEN: z.string().optional(),
/**
* HashiCorp Vault role ID for AppRole auth
* @category Config
*/
VAULT_ROLE_ID: z.string(),
/**
* HashiCorp Vault secret ID for AppRole auth
* @category Config
*/
VAULT_SECRET_ID: z.string(),
/**
* HashiCorp Vault secrets engine mount path
* @category Config
*/
VAULT_MOUNT_PATH: z.string().default("secret"),
/**
* HashiCorp Vault KV key
* @category Config
*/
VAULT_KV_KEY: z.string().default("K12"),
/**
* HashiCorp Vault API version
* @category Config
*/
VAULT_API_VERSION: z.enum(["v1", "v2"]).default("v1"),
/**
* Google Cloud Storage Configuration
* @category Config
*/
GCS_BUCKET_NAME: z.string().default("k12-core"),
GCS_PROJECT_ID: z.string().optional(),
/**
* Environment prefix for GCS paths (qa, dev, etc.)
* @category Config
*/
ENV_PREFIX: z.string().optional().default(""),
/**
* Object Image Processing Configuration
* @category Config
*/
OBJECT_IMAGE_SIZE: z.coerce.number().int().min(128).max(4096).default(1024),
OBJECT_IMAGE_QUALITY: z.coerce.number().int().min(1).max(100).default(85),
OBJECT_IMAGE_MAX_SIZE_MB: z.coerce.number().int().min(1).max(50).default(10),
OBJECT_IMAGE_FORMAT: z.nativeEnum(OBJECT_IMAGE_FORMATS).default(OBJECT_IMAGE_FORMATS.WEBP),
OBJECT_IMAGE_COMPRESSION_EFFORT: z.coerce.number().int().min(0).max(6).default(6),
/**
* Rate Limiting Configuration
* @category Config
*/
RATE_LIMIT_GENERAL_WINDOW_MS: z.coerce.number().int().min(1000).default(60000), // 1 minute
RATE_LIMIT_GENERAL_MAX: z.coerce.number().int().min(1).default(100),
RATE_LIMIT_AUTH_WINDOW_MS: z.coerce.number().int().min(1000).default(60000), // 1 minute
RATE_LIMIT_AUTH_MAX: z.coerce.number().int().min(1).default(10),
RATE_LIMIT_API_KEY_WINDOW_MS: z.coerce.number().int().min(1000).default(900000), // 15 minutes
RATE_LIMIT_API_KEY_MAX: z.coerce.number().int().min(1).default(1000),
RATE_LIMIT_UPLOAD_WINDOW_MS: z.coerce.number().int().min(1000).default(60000), // 1 minute
RATE_LIMIT_UPLOAD_MAX: z.coerce.number().int().min(1).default(20),
/**
* Physics service (RabbitMQ) host
* @category Config
*/
PHYSICS_ENGINE_HOST: z.string().default("136.116.75.152"),
/**
* Physics service RabbitMQ AMQP port
* @category Config
*/
PHYSICS_ENGINE_AMQP_PORT: z.coerce.number().default(5672),
/**
* Physics service RabbitMQ username
* @category Config
*/
PHYSICS_ENGINE_AMQP_USER: z.string().default("worker"),
/**
* Physics service RabbitMQ password
* @category Config
*/
PHYSICS_ENGINE_AMQP_PASSWORD: z.string().default("chrono2026"),
/**
* RabbitMQ queue name for physics commands (request). Chrono must consume with prefetch(1)
* so only one command is processed at a time (waiting = queued, running = unacked).
* @category Config
*/
PHYSICS_ENGINE_COMMAND_QUEUE: z.string().default("physics_engine_commands"),
/**
* RabbitMQ queue name for physics command replies (RPC). Per-connection queues use this as prefix + connectionId.
* @category Config
*/
PHYSICS_ENGINE_REPLY_QUEUE: z.string().default("physics_engine_replies"),
/**
* RabbitMQ topic exchange name for physics state broadcasts
* @category Config
*/
PHYSICS_ENGINE_STATE_EXCHANGE: z.string().default("physics_engine_broadcast"),
/**
* RabbitMQ queue name prefix for state broadcasts. Per-connection queue name is {prefix}_{connectionId}.
* @category Config
*/
PHYSICS_ENGINE_STATE_QUEUE: z.string().default("physics_engine_state"),
/**
* RabbitMQ connection heartbeat in seconds. Keeps connection alive over unstable networks.
* @category Config
*/
PHYSICS_ENGINE_HEARTBEAT: z.coerce.number().min(0).default(60),
/**
* Delay in ms before closing physics gateway (replies/state queues) after socket disconnect.
* Use 60000 (60s) or 300000 (300s) for stable reconnects on poor user internet.
* @category Config
*/
PHYSICS_GATEWAY_CLEANUP_DELAY_MS: z.coerce.number().min(0).default(60_000),
/**
* Max reconnection attempts for physics gateway (connection/channel drops).
* @category Config
*/
PHYSICS_GATEWAY_RECONNECT_ATTEMPTS: z.coerce.number().min(1).default(30),
/**
* Base delay in ms between reconnection attempts. Backoff may increase on repeated failures.
* @category Config
*/
PHYSICS_GATEWAY_RECONNECT_DELAY_MS: z.coerce.number().min(100).default(1000),
});
/**
* Parsed and validated environment variables.
* @category Config
*/
export const env = envSchema.parse(process.env);
Source