import zod from 'zod'; import { OpenTelemetryConfigurationModel } from '@hive/service-common'; import { createConnectionString } from '@hive/storage'; import { RequestBroker } from './lib/webhooks/send-webhook.js'; 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); }; const NumberFromString = zod.preprocess(numberFromNumberOrNumberString, zod.number().min(1)); // treat an empty string (`''`) as undefined const emptyString = (input: T) => { return zod.preprocess((value: unknown) => { if (value === '') return undefined; return value; }, input); }; const EnvironmentModel = zod.object({ PORT: emptyString(NumberFromString.optional()).default(3014), ENVIRONMENT: emptyString(zod.string().optional()), RELEASE: emptyString(zod.string().optional()), HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()), EMAIL_FROM: zod.string().email(), }); const SentryModel = zod.union([ zod.object({ SENTRY: emptyString(zod.literal('0').optional()), }), zod.object({ SENTRY: zod.literal('1'), SENTRY_DSN: zod.string(), }), ]); 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: emptyString(zod.string().optional()), }); const PostmarkEmailModel = zod.object({ EMAIL_PROVIDER: zod.literal('postmark'), EMAIL_PROVIDER_POSTMARK_TOKEN: zod.string(), EMAIL_PROVIDER_POSTMARK_MESSAGE_STREAM: zod.string(), }); const SMTPEmailModel = zod.object({ EMAIL_PROVIDER: zod.literal('smtp'), EMAIL_PROVIDER_SMTP_PROTOCOL: emptyString( zod.union([zod.literal('smtp'), zod.literal('smtps')]).optional(), ), EMAIL_PROVIDER_SMTP_HOST: zod.string(), EMAIL_PROVIDER_SMTP_PORT: NumberFromString, EMAIL_PROVIDER_SMTP_AUTH_USERNAME: zod.string(), EMAIL_PROVIDER_SMTP_AUTH_PASSWORD: zod.string(), EMAIL_PROVIDER_SMTP_REJECT_UNAUTHORIZED: emptyString( zod.union([zod.literal('0'), zod.literal('1')]).optional(), ), }); const SendmailEmailModel = zod.object({ EMAIL_PROVIDER: zod.literal('sendmail'), }); const MockEmailProviderModel = zod.object({ EMAIL_PROVIDER: zod.literal('mock'), }); const EmailProviderModel = zod.union([ PostmarkEmailModel, MockEmailProviderModel, SMTPEmailModel, SendmailEmailModel, ]); const RequestBrokerModel = zod.union([ zod.object({ REQUEST_BROKER: emptyString(zod.literal('0').optional()), }), zod.object({ REQUEST_BROKER: zod.literal('1'), REQUEST_BROKER_ENDPOINT: zod.string().min(1), REQUEST_BROKER_SIGNATURE: zod.string().min(1), }), ]); const PrometheusModel = zod.object({ PROMETHEUS_METRICS: emptyString( zod.union([zod.literal('0'), zod.literal('1')]).optional(), ).default('0'), PROMETHEUS_METRICS_LABEL_INSTANCE: emptyString(zod.string().optional()).default('workflows'), PROMETHEUS_METRICS_PORT: emptyString(NumberFromString.optional()).default(10254), }); const LogModel = zod.object({ LOG_LEVEL: emptyString( zod .union([ zod.literal('trace'), zod.literal('debug'), zod.literal('info'), zod.literal('warn'), zod.literal('error'), ]) .optional(), ), REQUEST_LOGGING: emptyString(zod.union([zod.literal('0'), zod.literal('1')]).optional()).default( '1', ), }); const configs = { base: EnvironmentModel.safeParse(process.env), email: EmailProviderModel.safeParse(process.env), sentry: SentryModel.safeParse(process.env), postgres: PostgresModel.safeParse(process.env), prometheus: PrometheusModel.safeParse(process.env), log: LogModel.safeParse(process.env), tracing: OpenTelemetryConfigurationModel.safeParse(process.env), requestBroker: RequestBrokerModel.safeParse(process.env), }; const environmentErrors: Array = []; 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(config: zod.SafeParseReturnType): Output { if (!config.success) { throw new Error('Something went wrong.'); } return config.data; } const base = extractConfig(configs.base); const email = extractConfig(configs.email); const postgres = extractConfig(configs.postgres); const sentry = extractConfig(configs.sentry); const prometheus = extractConfig(configs.prometheus); const log = extractConfig(configs.log); const tracing = extractConfig(configs.tracing); const requestBroker = extractConfig(configs.requestBroker); const emailProviderConfig = email.EMAIL_PROVIDER === 'postmark' ? ({ provider: 'postmark' as const, token: email.EMAIL_PROVIDER_POSTMARK_TOKEN, messageStream: email.EMAIL_PROVIDER_POSTMARK_MESSAGE_STREAM, } as const) : email.EMAIL_PROVIDER === 'smtp' ? ({ provider: 'smtp' as const, protocol: email.EMAIL_PROVIDER_SMTP_PROTOCOL ?? 'smtp', host: email.EMAIL_PROVIDER_SMTP_HOST, port: email.EMAIL_PROVIDER_SMTP_PORT, auth: { user: email.EMAIL_PROVIDER_SMTP_AUTH_USERNAME, pass: email.EMAIL_PROVIDER_SMTP_AUTH_PASSWORD, }, tls: { rejectUnauthorized: email.EMAIL_PROVIDER_SMTP_REJECT_UNAUTHORIZED !== '0', }, } as const) : email.EMAIL_PROVIDER === 'sendmail' ? ({ provider: 'sendmail' } as const) : ({ provider: 'mock' } as const); export type EmailProviderConfig = typeof emailProviderConfig; export type PostmarkEmailProviderConfig = Extract; export type SMTPEmailProviderConfig = Extract; export type SendmailEmailProviderConfig = Extract; export type MockEmailProviderConfig = Extract; export const env = { environment: base.ENVIRONMENT, release: base.RELEASE ?? 'local', http: { port: base.PORT ?? 6260, }, tracing: { enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT, collectorEndpoint: tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT, }, email: { provider: emailProviderConfig, emailFrom: base.EMAIL_FROM, }, sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null, log: { level: log.LOG_LEVEL ?? 'info', }, prometheus: prometheus.PROMETHEUS_METRICS === '1' ? { labels: { instance: prometheus.PROMETHEUS_METRICS_LABEL_INSTANCE ?? 'workflows', }, port: prometheus.PROMETHEUS_METRICS_PORT ?? 10_254, } : null, postgres: { connectionString: createConnectionString({ ssl: postgres.POSTGRES_SSL === '1', host: postgres.POSTGRES_HOST, db: postgres.POSTGRES_DB, password: postgres.POSTGRES_PASSWORD, port: postgres.POSTGRES_PORT, user: postgres.POSTGRES_USER, }), }, requestBroker: requestBroker.REQUEST_BROKER === '1' ? ({ endpoint: requestBroker.REQUEST_BROKER_ENDPOINT, signature: requestBroker.REQUEST_BROKER_SIGNATURE, } satisfies RequestBroker) : null, httpHeartbeat: base.HEARTBEAT_ENDPOINT ? { endpoint: base.HEARTBEAT_ENDPOINT } : null, } as const;