Source

config/env.js

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);