import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import { Global, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { z } from 'zod'; export const APP_CONFIG = Symbol('APP_CONFIG'); function findEnvironmentFile(): string { const directories = [ process.cwd(), resolve(process.cwd(), '..'), resolve(process.cwd(), '../..'), ]; const environmentFile = directories .map((directory) => resolve(directory, '.env')) .find((file) => existsSync(file)); return environmentFile ?? resolve(process.cwd(), '.env'); } // This schema is the only supported source for API runtime settings. Defaults are // intentionally limited to non-secret local-development values. const environmentSchema = z.object({ NODE_ENV: z .enum(['development', 'test', 'production']) .default('development'), API_PORT: z.coerce.number().int().min(1).max(65_535).default(8200), WEB_ORIGIN: z.url().default('http://localhost:8100'), DATABASE_URL: z.url(), REDIS_URL: z.url(), AUTH_JWT_SECRET: z.string().min(32), AUTH_RATE_LIMIT_KEY_SECRET: z.string().min(32), AUTH_PASSWORD_MIN_LENGTH: z.coerce .number() .int() .min(12) .max(256) .default(12), API_BODY_LIMIT_BYTES: z.coerce .number() .int() .min(1_024) .max(10 * 1024 * 1024) .default(1_048_576), AUTH_RATE_LIMIT_WINDOW_SECONDS: z.coerce .number() .int() .min(1) .max(86_400) .default(900), AUTH_LOGIN_IP_LIMIT: z.coerce.number().int().min(1).max(1000).default(10), AUTH_LOGIN_EMAIL_LIMIT: z.coerce.number().int().min(1).max(1000).default(5), AUTH_REGISTER_IP_LIMIT: z.coerce.number().int().min(1).max(1000).default(5), AUTH_REGISTER_EMAIL_LIMIT: z.coerce .number() .int() .min(1) .max(1000) .default(3), AUTH_REFRESH_IP_LIMIT: z.coerce .number() .int() .min(1) .max(10_000) .default(30), TRUSTED_PROXY_CIDRS: z.string().default(''), SWAGGER_ENABLED: z.enum(['true', 'false']).default('true'), LOG_LEVEL: z.enum(['error', 'warn', 'log', 'debug']).default('log'), }); type Environment = z.infer; export type AppConfig = Omit< Environment, 'TRUSTED_PROXY_CIDRS' | 'SWAGGER_ENABLED' > & { trustedProxyCidrs: string[]; swaggerEnabled: boolean; }; function validateEnvironment( environment: Record, ): Environment { const result = environmentSchema.safeParse(environment); if (result.success) return result.data; // Fail before Nest opens a listener; a half-configured API is unsafe to run. throw new Error( `Invalid environment configuration: ${result.error.issues.map((issue) => issue.path.join('.')).join(', ')}`, ); } function createAppConfig(environment: Environment): AppConfig { return { ...environment, // Express only trusts forwarding headers when this explicit allowlist is non-empty. trustedProxyCidrs: environment.TRUSTED_PROXY_CIDRS.split(',') .map((value) => value.trim()) .filter(Boolean), swaggerEnabled: environment.SWAGGER_ENABLED === 'true', }; } @Global() @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, cache: true, // Turbo runs this workspace's scripts from apps/api, while the // shared development environment lives at the repository root. envFilePath: findEnvironmentFile(), }), ], providers: [ { provide: APP_CONFIG, // Export a normalized immutable config object so services never read process.env directly. useFactory: (): AppConfig => createAppConfig(validateEnvironment(process.env)), }, ], exports: [APP_CONFIG], }) export class AppConfigModule {}