UNPKG

@accounter/server

Version:
316 lines (291 loc) 9.73 kB
import { config as dotenv } from 'dotenv'; import zod from 'zod'; // Prefer isolated test env file when provided, otherwise fall back to repo-level .env dotenv({ path: process.env.TEST_ENV_FILE && process.env.TEST_ENV_FILE.trim() !== '' ? process.env.TEST_ENV_FILE : ['.env', '../../.env'], debug: process.env.RELEASE ? false : true, }); const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success; const numberFromNumberOrNumberString = (input: unknown): number | undefined => { if (typeof input === 'number') return input; if (isNumberString(input)) return Number(input); return undefined; }; const NumberFromString = zod.preprocess(numberFromNumberOrNumberString, zod.number().min(1)); // treat an empty string (`''`) as undefined const emptyString = <T extends zod.ZodType>(input: T) => { return zod.preprocess((value: unknown) => { if (value === '') return undefined; return value; }, input); }; const PostgresModel = zod.object({ POSTGRES_SSL: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), POSTGRES_HOST: zod.string(), POSTGRES_PORT: NumberFromString, POSTGRES_DB: zod.string(), POSTGRES_USER: zod.string(), POSTGRES_PASSWORD: zod.string(), POSTGRES_MAX_CLIENTS: emptyString(NumberFromString).optional().default(20), }); const CloudinaryModel = zod.union([ zod .object({ CLOUDINARY_NAME: zod.string().optional(), CLOUDINARY_API_KEY: zod.string().optional(), CLOUDINARY_API_SECRET: zod.string().optional(), }) .superRefine((data, ctx) => { if ( !!data.CLOUDINARY_NAME !== !!data.CLOUDINARY_API_KEY || !!data.CLOUDINARY_NAME !== !!data.CLOUDINARY_API_SECRET ) { ctx.addIssue({ code: 'custom', message: 'CLOUDINARY_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET must be provided together.', }); } }), zod.void(), ]); const GreenInvoiceModel = zod.union([ zod .object({ GREEN_INVOICE_ID: zod.string().optional(), GREEN_INVOICE_SECRET: zod.string().optional(), }) .superRefine((data, ctx) => { if (!!data.GREEN_INVOICE_ID !== !!data.GREEN_INVOICE_SECRET) { ctx.addIssue({ code: 'custom', message: 'GREEN_INVOICE_ID and GREEN_INVOICE_SECRET must be provided together.', }); } }), zod.void(), ]); const HiveModel = zod.union([ zod.object({ HIVE_TOKEN: zod.string().optional(), }), zod.void(), ]); const GoogleDriveModel = zod.union([ zod.object({ GOOGLE_DRIVE_API_KEY: zod.string().optional(), }), zod.void(), ]); const DeelModel = zod.union([ zod.object({ DEEL_TOKEN: zod.string().optional(), }), zod.void(), ]); const CredentialsModel = zod.object({ CREDENTIALS_ENCRYPTION_KEY: zod .string() .trim() .regex(/^[a-f0-9]{64}$/i), }); const GeneralModel = zod.object({ FRONTEND_URL: zod.url().optional(), }); const OtelModel = zod .object({ OTEL_ENABLED: emptyString( zod .union([zod.literal('1'), zod.literal('0')]) .optional() .default('0'), ), OTEL_SERVICE_NAME: emptyString(zod.string().optional().default('accounter-server')), OTEL_SERVICE_NAMESPACE: emptyString(zod.string().optional().default('accounter')), OTEL_DEPLOYMENT_ENV: emptyString( zod .string() .optional() .default(process.env.NODE_ENV ?? 'development'), ), OTEL_EXPORTER_OTLP_ENDPOINT: emptyString(zod.string().optional()), OTEL_EXPORTER_OTLP_HEADERS: emptyString(zod.string().optional()), OTEL_TRACES_SAMPLER: emptyString( zod .enum([ 'parentbased_traceidratio', 'always_on', 'always_off', 'traceidratio', 'parentbased_always_on', 'parentbased_always_off', ]) .optional() .default('always_on'), ), OTEL_TRACES_SAMPLER_ARG: emptyString(zod.string().optional()), OTEL_STARTUP_STRICT: emptyString( zod.union([zod.literal('true'), zod.literal('false')]).optional(), ), }) .superRefine((data, ctx) => { if (data.OTEL_ENABLED === '1' && !data.OTEL_EXPORTER_OTLP_ENDPOINT) { ctx.addIssue({ code: 'custom', path: ['OTEL_EXPORTER_OTLP_ENDPOINT'], message: 'OTEL_EXPORTER_OTLP_ENDPOINT is required when OTEL_ENABLED is "1".', }); } const usesRatioSampler = data.OTEL_TRACES_SAMPLER === 'parentbased_traceidratio' || data.OTEL_TRACES_SAMPLER === 'traceidratio'; if (usesRatioSampler) { if (data.OTEL_TRACES_SAMPLER_ARG === undefined) { ctx.addIssue({ code: 'custom', path: ['OTEL_TRACES_SAMPLER_ARG'], message: 'OTEL_TRACES_SAMPLER_ARG is required when OTEL_TRACES_SAMPLER is a ratio sampler ("traceidratio" or "parentbased_traceidratio").', }); } else { const num = Number(data.OTEL_TRACES_SAMPLER_ARG); if (!Number.isFinite(num) || num < 0 || num > 1) { ctx.addIssue({ code: 'custom', path: ['OTEL_TRACES_SAMPLER_ARG'], message: 'OTEL_TRACES_SAMPLER_ARG must be a numeric string between 0 and 1.', }); } } } }); const Auth0Model = zod.union([ zod.object({ AUTH0_DOMAIN: zod.string().min(1), AUTH0_AUDIENCE: zod.string().min(1), AUTH0_CLIENT_ID: zod.string().min(1), AUTH0_CLIENT_SECRET: zod.string().min(1), AUTH0_MANAGEMENT_AUDIENCE: zod.string().min(1), }), // If no Auth0 variables are provided, validation passes (optional configuration) // We use a looser object check here because process.env is always an object zod .object({ AUTH0_DOMAIN: zod.literal('').optional(), AUTH0_AUDIENCE: zod.literal('').optional(), AUTH0_CLIENT_ID: zod.literal('').optional(), AUTH0_CLIENT_SECRET: zod.literal('').optional(), AUTH0_MANAGEMENT_AUDIENCE: zod.literal('').optional(), }) .transform(() => undefined), ]); const configs = { postgres: PostgresModel.safeParse(process.env), cloudinary: CloudinaryModel.safeParse(process.env), greenInvoice: GreenInvoiceModel.safeParse(process.env), hive: HiveModel.safeParse(process.env), googleDrive: GoogleDriveModel.safeParse(process.env), auth0: Auth0Model.safeParse(process.env), deel: DeelModel.safeParse(process.env), credentials: CredentialsModel.safeParse(process.env), general: GeneralModel.safeParse(process.env), otel: OtelModel.safeParse(process.env), }; const environmentErrors: Array<string> = []; for (const config of Object.values(configs)) { if (config.success === false) { environmentErrors.push(JSON.stringify(config.error.format(), null, 4)); } } if (environmentErrors.length) { const fullError = environmentErrors.join(`\n`); console.error('❌ Invalid environment variables:', fullError); process.exit(1); } function extractConfig<Output>(config: zod.ZodSafeParseResult<Output>): Output { if (!config.success) { throw new Error('Something went wrong.'); } return config.data; } const postgres = extractConfig(configs.postgres); const cloudinary = extractConfig(configs.cloudinary); const greenInvoice = extractConfig(configs.greenInvoice); const hive = extractConfig(configs.hive); const googleDrive = extractConfig(configs.googleDrive); const auth0 = extractConfig(configs.auth0); const deel = extractConfig(configs.deel); const credentials = extractConfig(configs.credentials); const general = extractConfig(configs.general); const otel = extractConfig(configs.otel); export const env = { postgres: { host: postgres.POSTGRES_HOST, port: postgres.POSTGRES_PORT, db: postgres.POSTGRES_DB, user: postgres.POSTGRES_USER, password: postgres.POSTGRES_PASSWORD, ssl: postgres.POSTGRES_SSL === '1', max: postgres.POSTGRES_MAX_CLIENTS, }, cloudinary: cloudinary?.CLOUDINARY_API_KEY ? { name: cloudinary.CLOUDINARY_NAME!, apiKey: cloudinary.CLOUDINARY_API_KEY!, apiSecret: cloudinary.CLOUDINARY_API_SECRET!, } : undefined, greenInvoice: greenInvoice?.GREEN_INVOICE_ID ? { id: greenInvoice.GREEN_INVOICE_ID!, secret: greenInvoice.GREEN_INVOICE_SECRET!, } : undefined, hive: hive?.HIVE_TOKEN ? { hiveToken: hive.HIVE_TOKEN!, } : undefined, googleDrive: googleDrive?.GOOGLE_DRIVE_API_KEY ? { driveApiKey: googleDrive.GOOGLE_DRIVE_API_KEY!, } : undefined, deel: deel?.DEEL_TOKEN ? { apiToken: deel.DEEL_TOKEN!, } : undefined, credentialsEncryptionKey: credentials.CREDENTIALS_ENCRYPTION_KEY, auth0: auth0 ? { domain: auth0.AUTH0_DOMAIN, audience: auth0.AUTH0_AUDIENCE, clientId: auth0.AUTH0_CLIENT_ID, clientSecret: auth0.AUTH0_CLIENT_SECRET, managementAudience: auth0.AUTH0_MANAGEMENT_AUDIENCE, } : undefined, general: { frontendUrl: general?.FRONTEND_URL, }, otel: { enabled: otel.OTEL_ENABLED === '1', serviceName: otel.OTEL_SERVICE_NAME, serviceNamespace: otel.OTEL_SERVICE_NAMESPACE, deploymentEnv: otel.OTEL_DEPLOYMENT_ENV, exporterEndpoint: otel.OTEL_EXPORTER_OTLP_ENDPOINT, exporterHeaders: otel.OTEL_EXPORTER_OTLP_HEADERS, tracesSampler: otel.OTEL_TRACES_SAMPLER, tracesSamplerArg: (otel.OTEL_TRACES_SAMPLER === 'parentbased_traceidratio' || otel.OTEL_TRACES_SAMPLER === 'traceidratio') && otel.OTEL_TRACES_SAMPLER_ARG !== undefined ? Number(otel.OTEL_TRACES_SAMPLER_ARG) : undefined, startupStrict: otel.OTEL_STARTUP_STRICT === 'true', }, } as const;