| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- import { z } from 'zod';
- /**
- * Environment contract for the relay. Validation happens once at boot: a
- * missing/typo'd variable must fail loudly at startup, never mid-conversation.
- */
- /** `.default()` has to precede `.transform()` in zod v4, hence the factories. */
- const boolish = (def: string) =>
- z
- .string()
- .default(def)
- .transform((v) => ['1', 'true', 'yes', 'on'].includes(v.trim().toLowerCase()));
- const csv = (def: string) =>
- z
- .string()
- .default(def)
- .transform((v) =>
- v
- .split(',')
- .map((s) => s.trim().toUpperCase())
- .filter(Boolean),
- );
- const schema = z.object({
- NODE_ENV: z.enum(['development', 'test', 'production']).default('production'),
- PORT: z.coerce.number().int().positive().default(3000),
- RELAY_MODE: z.enum(['dev', 'prod']).default('prod'),
- DATABASE_URL: z.string().min(1).default('file:../data/eks_relay.db'),
- // --- Chatwoot ---
- CHATWOOT_BASE_URL: z.string().url(),
- CHATWOOT_API_TOKEN: z.string().min(1),
- CHATWOOT_ACCOUNT_ID: z.coerce.number().int().positive().default(1),
- CHATWOOT_TICKET_LABEL: z.string().min(1).default('ticket'),
- CHATWOOT_SPAM_LABEL: z.string().default('spam'),
- CHATWOOT_APPLY_SPAM_LABEL: boolish('false'),
- CHATWOOT_UNASSIGN_ON_TICKET: boolish('false'),
- /// Webchat / API inbox — reserved for the later web widget adapter.
- CHATWOOT_API_INBOX_ID: z.string().default(''),
- CHATWOOT_API_IDENTITY_VALIDATION_TOKEN: z.string().default(''),
- // --- Flowise ---
- FLOWISE_PREDICT_URL: z.string().url(),
- FLOWISE_API_KEY: z.string().default(''),
- FLOWISE_BASE_URL: z.string().default(''),
- FLOWISE_TIMEOUT_MS: z.coerce.number().int().positive().default(90_000),
- // --- WooCommerce / WordPress ---
- WOOCOMMERCE_BASE_URL: z.string().url(),
- WOOCOMMERCE_CONSUMER_KEY: z.string().min(1),
- WOOCOMMERCE_CONSUMER_SECRET: z.string().min(1),
- WP_STORE_API_SECRET: z.string().default(''),
- STORE_CURRENCIES: csv('PLN,EUR,AED,CZK,HUF,DKK,SEK,NOK,RON,BGN,GBP'),
- // --- Relay auth ---
- RELAY_SHARED_SECRET: z.string().min(8),
- ADMIN_TOKEN: z.string().default(''),
- // --- Behaviour ---
- TENANT_ID: z.string().default('easyklima'),
- DEFAULT_LANGUAGE: z.string().default('pl'),
- TICKET_NUMBER_PREFIX: z.string().default('EKS'),
- WORKER_ENABLED: boolish('true'),
- WORKER_POLL_MS: z.coerce.number().int().positive().default(1_000),
- WORKER_MAX_ATTEMPTS: z.coerce.number().int().positive().default(3),
- SPAM_GATE_ENABLED: boolish('true'),
- HTTP_TIMEOUT_MS: z.coerce.number().int().positive().default(30_000),
- // --- Logging ---
- LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
- LOG_PII: boolish('false'),
- });
- export type Config = z.infer<typeof schema>;
- let cached: Config | null = null;
- export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
- const parsed = schema.safeParse(env);
- if (!parsed.success) {
- // Only names of the offending variables — never their values.
- const issues = parsed.error.issues
- .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
- .join('; ');
- throw new Error(`Invalid environment configuration: ${issues}`);
- }
- return parsed.data;
- }
- export function config(): Config {
- cached ??= loadConfig();
- return cached;
- }
- /** Test hook: inject a config without touching process.env. */
- export function setConfigForTests(cfg: Config): void {
- cached = cfg;
- }
|