config.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { z } from 'zod';
  2. /**
  3. * Environment contract for the relay. Validation happens once at boot: a
  4. * missing/typo'd variable must fail loudly at startup, never mid-conversation.
  5. */
  6. /** `.default()` has to precede `.transform()` in zod v4, hence the factories. */
  7. const boolish = (def: string) =>
  8. z
  9. .string()
  10. .default(def)
  11. .transform((v) => ['1', 'true', 'yes', 'on'].includes(v.trim().toLowerCase()));
  12. const csv = (def: string) =>
  13. z
  14. .string()
  15. .default(def)
  16. .transform((v) =>
  17. v
  18. .split(',')
  19. .map((s) => s.trim().toUpperCase())
  20. .filter(Boolean),
  21. );
  22. const schema = z.object({
  23. NODE_ENV: z.enum(['development', 'test', 'production']).default('production'),
  24. PORT: z.coerce.number().int().positive().default(3000),
  25. RELAY_MODE: z.enum(['dev', 'prod']).default('prod'),
  26. DATABASE_URL: z.string().min(1).default('file:../data/eks_relay.db'),
  27. // --- Chatwoot ---
  28. CHATWOOT_BASE_URL: z.string().url(),
  29. CHATWOOT_API_TOKEN: z.string().min(1),
  30. CHATWOOT_ACCOUNT_ID: z.coerce.number().int().positive().default(1),
  31. CHATWOOT_TICKET_LABEL: z.string().min(1).default('ticket'),
  32. CHATWOOT_SPAM_LABEL: z.string().default('spam'),
  33. CHATWOOT_APPLY_SPAM_LABEL: boolish('false'),
  34. CHATWOOT_UNASSIGN_ON_TICKET: boolish('false'),
  35. /// Webchat / API inbox — reserved for the later web widget adapter.
  36. CHATWOOT_API_INBOX_ID: z.string().default(''),
  37. CHATWOOT_API_IDENTITY_VALIDATION_TOKEN: z.string().default(''),
  38. // --- Flowise ---
  39. FLOWISE_PREDICT_URL: z.string().url(),
  40. FLOWISE_API_KEY: z.string().default(''),
  41. FLOWISE_BASE_URL: z.string().default(''),
  42. FLOWISE_TIMEOUT_MS: z.coerce.number().int().positive().default(90_000),
  43. // --- WooCommerce / WordPress ---
  44. WOOCOMMERCE_BASE_URL: z.string().url(),
  45. WOOCOMMERCE_CONSUMER_KEY: z.string().min(1),
  46. WOOCOMMERCE_CONSUMER_SECRET: z.string().min(1),
  47. WP_STORE_API_SECRET: z.string().default(''),
  48. STORE_CURRENCIES: csv('PLN,EUR,AED,CZK,HUF,DKK,SEK,NOK,RON,BGN,GBP'),
  49. // --- Relay auth ---
  50. RELAY_SHARED_SECRET: z.string().min(8),
  51. ADMIN_TOKEN: z.string().default(''),
  52. // --- Behaviour ---
  53. TENANT_ID: z.string().default('easyklima'),
  54. DEFAULT_LANGUAGE: z.string().default('pl'),
  55. TICKET_NUMBER_PREFIX: z.string().default('EKS'),
  56. WORKER_ENABLED: boolish('true'),
  57. WORKER_POLL_MS: z.coerce.number().int().positive().default(1_000),
  58. WORKER_MAX_ATTEMPTS: z.coerce.number().int().positive().default(3),
  59. SPAM_GATE_ENABLED: boolish('true'),
  60. HTTP_TIMEOUT_MS: z.coerce.number().int().positive().default(30_000),
  61. // --- Logging ---
  62. LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
  63. LOG_PII: boolish('false'),
  64. });
  65. export type Config = z.infer<typeof schema>;
  66. let cached: Config | null = null;
  67. export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
  68. const parsed = schema.safeParse(env);
  69. if (!parsed.success) {
  70. // Only names of the offending variables — never their values.
  71. const issues = parsed.error.issues
  72. .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
  73. .join('; ');
  74. throw new Error(`Invalid environment configuration: ${issues}`);
  75. }
  76. return parsed.data;
  77. }
  78. export function config(): Config {
  79. cached ??= loadConfig();
  80. return cached;
  81. }
  82. /** Test hook: inject a config without touching process.env. */
  83. export function setConfigForTests(cfg: Config): void {
  84. cached = cfg;
  85. }