From 12329976c391c9e80ec36ee0afe1bc4cc465188a Mon Sep 17 00:00:00 2001 From: reversegem_7 Date: Fri, 6 Jun 2025 15:15:12 +0200 Subject: [PATCH 1/2] refactor(env): replace dotenv with zod for type-safe env validation - Remove dotenv dependency and deprecated env.d.ts type declarations - Implement Zod schema for runtime validation and type inference - Add global type augmentation for process.env - Update deploy scripts to use --env-file flag --- package.json | 8 ++--- src/lib/setup/env.ts | 73 ++++++++++++++++++++++++++++++++++++++++-- src/lib/types/env.d.ts | 25 --------------- 3 files changed, 75 insertions(+), 31 deletions(-) delete mode 100644 src/lib/types/env.d.ts diff --git a/package.json b/package.json index b4dcb83..7fac777 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ }, "scripts": { "format": "prettier --write \"src/**/*.ts\"", - "deploy": "bun .", - "deploy:dev": "tsc --noEmit && DEV_DEPLOYMENT=true bun ." + "deploy": "bun --env-file=.env .", + "deploy:dev": "tsc --noEmit && DEV_DEPLOYMENT=true bun --env-file=.env ." }, "dependencies": { "@sapphire/discord-utilities": "~3.3.0", @@ -28,13 +28,13 @@ "openblox": "~1.0.59", "puppeteer": "23.3.0", "puppeteer-cluster": "~0.24.0", - "redis": "~4.6.14" + "redis": "~4.6.14", + "zod": "^3.25.55" }, "devDependencies": { "@sapphire/decorators": "~6.1.0", "@types/node": "~20.14.4", "@typical-developers/api-types": "~0.1.19", - "dotenv": "~16.4.5", "prettier": "~3.3.2", "rimraf": "~5.0.7", "typescript": "5.4.5" diff --git a/src/lib/setup/env.ts b/src/lib/setup/env.ts index d274743..0a151fd 100644 --- a/src/lib/setup/env.ts +++ b/src/lib/setup/env.ts @@ -1,3 +1,72 @@ -import dotenv from 'dotenv'; +import { z, type ZodTypeAny } from 'zod'; -dotenv.config(); +/** + * Validates and type-checks environment variables using a Zod schema. + * + * This function ensures that all required environment variables are present and correctly typed. + * If validation fails, the process will exit with an error message. + * + * @param schema - Zod schema defining the structure and types of environment variables + * @param env - Optional environment object to validate (defaults to process.env) + * @returns Validated and typed environment object + * @throws Exits process if validation fails + * @example + * ```typescript + * const schema = z.object({ + * DISCORD_TOKEN: z.string(), + * PORT: z.number() + * }); + * + * const env = Env(schema); + * console.log(env.DISCORD_TOKEN); // Fully typed access to env vars + * ``` + * + */ +export function Env(schema: T, env: Record = process.env): z.infer { + const result = schema.safeParse(env); + if (!result.success) { + console.error('❌ Invalid environment variables:', result.error.format()); + process.exit(1); // Exit the process if validation fails to prevent further execution. + } + + return result.data; +} + +const envVariables = z.object({ + DISCORD_TOKEN: z.string(), + + BOT_ENDPOINT_API_KEY: z.string(), + EXPERIENCE_ENDPOINT_API_SECRET: z.string(), + BOT_ERROR_WEBHOOK_URL: z.string(), + + REDIS_USERNAME: z.string(), + REDIS_PASSWORD: z.string(), + REDIS_HOST: z.string() +}); + +/** + * Currently using process.env only to maintain compatibility with existing files + * + * TODO: Consider refactoring to use the typed env object directly for better type safety + * const env = Env(envVariables); + * console.log(env.DISCORD_TOKEN); // Fully typed access to env vars + */ + +Env(envVariables); + +// Typed proccess.env +declare global { + namespace NodeJS { + interface ProcessEnv extends z.infer {} + } +} + +/** + * If you prefer to use bun instead of node, you can use this instead: + * + * ```typescript + * declare module 'bun' { + * interface Env extends z.infer {} + * } + * ``` + */ diff --git a/src/lib/types/env.d.ts b/src/lib/types/env.d.ts deleted file mode 100644 index fd479cc..0000000 --- a/src/lib/types/env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -declare namespace NodeJS { - interface ProcessEnv { - /** The API url. */ - API_URL: string; - - /** Whether or not the current deployment is a dev build. */ - DEV_DEPLOYMENT: 'true' | 'false'; - - /** The Discord bot's token. */ - DISCORD_TOKEN: string; - /** The API key for accessing the bot's backend. */ - BOT_ENDPOINT_API_KEY: string; - /** The API secret for accessing the experience's backend. */ - EXPERIENCE_ENDPOINT_API_SECRET: string; - /** The webhook for logging errors. */ - BOT_ERROR_WEBHOOK_URL: string; - - /** Redis Username */ - REDIS_USERNAME: string; - /** Redis Password */ - REDIS_PASSWORD: string; - /** Redis host (host:port) */ - REDIS_HOST: string; - } -} From 321d3ccf4c9bac1ab84775e37e343cc0c55ace74 Mon Sep 17 00:00:00 2001 From: reversegem_7 Date: Fri, 6 Jun 2025 16:48:09 +0200 Subject: [PATCH 2/2] fix(env): check API_URL and DEV_DEPLOYMENT env variables on env.ts - Add API_URL to .env.example --- .env.example | 4 +++- src/lib/setup/env.ts | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 1f9e22d..6ac8758 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ +API_URL= + DISCORD_TOKEN= BOT_ENDPOINT_API_KEY= @@ -7,4 +9,4 @@ BOT_ERROR_WEBHOOK_URL= # Redis REDIS_USERNAME= REDIS_PASSWORD= -REDIS_HOST= \ No newline at end of file +REDIS_HOST= diff --git a/src/lib/setup/env.ts b/src/lib/setup/env.ts index 0a151fd..14d7a8b 100644 --- a/src/lib/setup/env.ts +++ b/src/lib/setup/env.ts @@ -37,11 +37,14 @@ const envVariables = z.object({ BOT_ENDPOINT_API_KEY: z.string(), EXPERIENCE_ENDPOINT_API_SECRET: z.string(), - BOT_ERROR_WEBHOOK_URL: z.string(), + BOT_ERROR_WEBHOOK_URL: z.string().url(), REDIS_USERNAME: z.string(), REDIS_PASSWORD: z.string(), - REDIS_HOST: z.string() + REDIS_HOST: z.string(), + + API_URL: z.string().url(), + DEV_DEPLOYMENT: z.union([z.literal('true'), z.literal('false')]).default('false') }); /**