From 5080beaeebd7ee0f9f25bb3c1d5331cfbde4ab52 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 4 Aug 2026 02:50:48 -0400 Subject: [PATCH] fix: standardize boolean environment parsing --- next.config.js | 6 +++++- src/env.ts | 20 ++++++++++---------- src/server/auth.ts | 3 ++- src/tests/env.test.ts | 27 +++++++++++++++++++++++++++ src/utils/env.ts | 16 ++++++++++++++++ 5 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 src/tests/env.test.ts create mode 100644 src/utils/env.ts diff --git a/next.config.js b/next.config.js index 7c59f30b..2b98da87 100644 --- a/next.config.js +++ b/next.config.js @@ -5,6 +5,10 @@ import { createJiti } from 'jiti'; const jiti = createJiti(fileURLToPath(import.meta.url)); import withSerwistInit from '@serwist/next'; +/** @type {typeof import('./src/utils/env')} */ +const envUtils = await jiti.import('./src/utils/env'); +const { parseEnvBoolean } = envUtils; + /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful * for Docker builds. @@ -14,7 +18,7 @@ await jiti.import('./src/env'); /** @type {import("next").NextConfig} */ const nextConfig = { reactStrictMode: true, - output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined, + output: parseEnvBoolean(process.env.DOCKER_OUTPUT) ? 'standalone' : undefined, transpilePackages: ['@t3-oss/env-nextjs', '@t3-oss/env-core'], /** * If you are using `appDir` then you must comment the below `i18n` config out. diff --git a/src/env.ts b/src/env.ts index 913ed31c..3dc8370c 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,6 +1,8 @@ import { createEnv } from '@t3-oss/env-nextjs'; import { z } from 'zod'; +import { parseEnvBoolean } from './utils/env'; + export const env = createEnv({ /** * Specify your server-side environment variables schema here. This way you can ensure the app @@ -93,23 +95,21 @@ export const env = createEnv({ process.env.DATABASE_URL ?? `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST}:${process.env.POSTGRES_PORT}`, NODE_ENV: process.env.NODE_ENV, - DOCKER_OUTPUT: Boolean(JSON.parse(process.env.DOCKER_OUTPUT || 'false')), + DOCKER_OUTPUT: parseEnvBoolean(process.env.DOCKER_OUTPUT), NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, NEXTAUTH_URL: process.env.NEXTAUTH_URL, NEXTAUTH_URL_INTERNAL: process.env.NEXTAUTH_URL_INTERNAL ?? process.env.NEXTAUTH_URL, CLEAR_CACHE_CRON_RULE: process.env.CLEAR_CACHE_CRON_RULE ?? '0 2 * * 0', CACHE_RETENTION_INTERVAL: process.env.CACHE_RETENTION_INTERVAL ?? '2 days', - ENABLE_SENDING_INVITES: 'true' === process.env.ENABLE_SENDING_INVITES, - DISABLE_EMAIL_SIGNUP: 'true' === process.env.DISABLE_EMAIL_SIGNUP, - INVITE_ONLY: 'true' === process.env.INVITE_ONLY, + ENABLE_SENDING_INVITES: parseEnvBoolean(process.env.ENABLE_SENDING_INVITES), + DISABLE_EMAIL_SIGNUP: parseEnvBoolean(process.env.DISABLE_EMAIL_SIGNUP), + INVITE_ONLY: parseEnvBoolean(process.env.INVITE_ONLY), FROM_EMAIL: process.env.FROM_EMAIL, EMAIL_SERVER_HOST: process.env.EMAIL_SERVER_HOST, EMAIL_SERVER_PORT: process.env.EMAIL_SERVER_PORT, EMAIL_SERVER_USER: process.env.EMAIL_SERVER_USER, EMAIL_SERVER_PASSWORD: process.env.EMAIL_SERVER_PASSWORD, - EMAIL_TLS_REJECT_UNAUTHORIZED: Boolean( - JSON.parse(process.env.EMAIL_TLS_REJECT_UNAUTHORIZED || 'true'), - ), + EMAIL_TLS_REJECT_UNAUTHORIZED: parseEnvBoolean(process.env.EMAIL_TLS_REJECT_UNAUTHORIZED, true), GOCARDLESS_COUNTRY: process.env.GOCARDLESS_COUNTRY, GOCARDLESS_SECRET_ID: process.env.GOCARDLESS_SECRET_ID, GOCARDLESS_SECRET_KEY: process.env.GOCARDLESS_SECRET_KEY, @@ -139,8 +139,8 @@ export const env = createEnv({ OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID, OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET, OIDC_WELL_KNOWN_URL: process.env.OIDC_WELL_KNOWN_URL, - OIDC_ALLOW_DANGEROUS_EMAIL_LINKING: Boolean( - JSON.parse(process.env.OIDC_ALLOW_DANGEROUS_EMAIL_LINKING || 'false'), + OIDC_ALLOW_DANGEROUS_EMAIL_LINKING: parseEnvBoolean( + process.env.OIDC_ALLOW_DANGEROUS_EMAIL_LINKING, ), UPLOAD_MAX_FILE_SIZE_MB: process.env.UPLOAD_MAX_FILE_SIZE_MB ? Number(process.env.UPLOAD_MAX_FILE_SIZE_MB) @@ -152,7 +152,7 @@ export const env = createEnv({ * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially * useful for Docker builds. */ - skipValidation: Boolean(JSON.parse(process.env.SKIP_ENV_VALIDATION || 'false')), + skipValidation: parseEnvBoolean(process.env.SKIP_ENV_VALIDATION), /** * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and * `SOME_VAR=''` will throw an error. diff --git a/src/server/auth.ts b/src/server/auth.ts index bbc73e30..c629ec36 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -10,6 +10,7 @@ import KeycloakProvider from 'next-auth/providers/keycloak'; import { env } from '~/env'; import { db } from '~/server/db'; +import { parseEnvBoolean } from '~/utils/env'; import { mailServerConfig, sendSignUpEmail } from './mailer'; import { getBaseUrl } from '~/utils/api'; @@ -281,7 +282,7 @@ function getProviders() { */ export function validateAuthEnv() { console.log('Validating auth env'); - if (!process.env.SKIP_ENV_VALIDATION) { + if (!parseEnvBoolean(process.env.SKIP_ENV_VALIDATION)) { const providers = getProviders(); if (0 === providers.length) { throw new Error( diff --git a/src/tests/env.test.ts b/src/tests/env.test.ts new file mode 100644 index 00000000..cc3a6c91 --- /dev/null +++ b/src/tests/env.test.ts @@ -0,0 +1,27 @@ +import { parseEnvBoolean } from '../utils/env'; + +describe('parseEnvBoolean', () => { + it.each([ + ['1', true], + ['true', true], + ['TRUE', true], + [' TrUe ', true], + ['0', false], + ['false', false], + ['FALSE', false], + [' FaLsE ', false], + ])('parses %p as %p', (value, expected) => { + expect(parseEnvBoolean(value)).toBe(expected); + }); + + it.each([undefined, '', 'on', 'off', 'yes', '2'])('uses false for invalid value %p', (value) => { + expect(parseEnvBoolean(value)).toBe(false); + }); + + it.each([undefined, '', 'on', 'off', 'yes', '2'])( + 'uses the supplied fallback for invalid value %p', + (value) => { + expect(parseEnvBoolean(value, true)).toBe(true); + }, + ); +}); diff --git a/src/utils/env.ts b/src/utils/env.ts new file mode 100644 index 00000000..6208a5e7 --- /dev/null +++ b/src/utils/env.ts @@ -0,0 +1,16 @@ +const TRUE_ENV_VALUES = new Set(['1', 'true']); +const FALSE_ENV_VALUES = new Set(['0', 'false']); + +export const parseEnvBoolean = (value: string | undefined, fallback = false): boolean => { + const normalizedValue = value?.trim().toLowerCase(); + + if (TRUE_ENV_VALUES.has(normalizedValue ?? '')) { + return true; + } + + if (FALSE_ENV_VALUES.has(normalizedValue ?? '')) { + return false; + } + + return fallback; +};