/** * @fileoverview Process configuration for the Verae Zapier middleware. * @module config * * Loads optional `.env` then exports typed settings used by HTTP, NATS, and workers. */ import { readFileSync, existsSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createDebugger } from './debug/logger.js'; const log = createDebugger('app'); const __dirname = dirname(fileURLToPath(import.meta.url)); const rootDir = resolve(__dirname, '..'); /** * Load KEY=VALUE pairs from `.env` without overriding existing process.env keys. * @returns {void} */ export function loadEnvFile() { const envPath = resolve(rootDir, '.env'); if (!existsSync(envPath)) return; for (const line of readFileSync(envPath, 'utf8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq === -1) continue; const key = trimmed.slice(0, eq).trim(); const value = trimmed.slice(eq + 1).trim(); if (!(key in process.env)) { process.env[key] = value; } } } loadEnvFile(); /** * @param {string|undefined} value * @param {boolean} [fallback=false] * @returns {boolean} */ function bool(value, fallback = false) { if (value === undefined) return fallback; return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase()); } /** * @param {string|undefined} value * @param {number} fallback * @returns {number} */ function int(value, fallback) { const parsed = Number.parseInt(value ?? '', 10); return Number.isFinite(parsed) ? parsed : fallback; } /** * Runtime configuration object. * @type {{ * port: number, * host: string, * veraeApiBaseUrl: string, * mockVerae: boolean, * natsEnabled: boolean, * natsUrl: string, * tokenSecret: string, * jobPollIntervalMs: number, * jobPollMaxAttempts: number, * storePath: string, * upgradeUrl: string, * adminSecret: string, * }} */ export const config = { port: int(process.env.PORT, 3100), host: process.env.HOST ?? '0.0.0.0', veraeApiBaseUrl: (process.env.VERAE_API_BASE_URL ?? 'http://localhost:8080').replace( /\/$/, '', ), mockVerae: bool(process.env.MOCK_VERAE, false), natsEnabled: bool(process.env.NATS_ENABLED, false), natsUrl: process.env.NATS_URL ?? 'nats://127.0.0.1:4222', tokenSecret: process.env.TOKEN_SECRET ?? 'dev-secret-change-me', jobPollIntervalMs: int(process.env.JOB_POLL_INTERVAL_MS, 2000), jobPollMaxAttempts: int(process.env.JOB_POLL_MAX_ATTEMPTS, 60), waitTimeoutMs: int(process.env.WAIT_TIMEOUT_MS, 25000), mockJobCompleteMs: int(process.env.MOCK_JOB_COMPLETE_MS, 150), storePath: resolve(rootDir, process.env.STORE_PATH ?? './data/store.json'), upgradeUrl: process.env.UPGRADE_URL ?? 'https://veraetime.net/billing', adminSecret: process.env.ADMIN_SECRET ?? 'change-me-admin', }; /** * Default plan limits. `null` numeric fields mean unlimited. * @type {Record} */ export const PLAN_LIMITS = { free: { timestamps: 50, verifications: 50, batch: false, batchMaxItems: 0, requestsPerMinute: 30, }, starter: { timestamps: 500, verifications: 500, batch: true, batchMaxItems: 10, requestsPerMinute: 120, }, pro: { timestamps: 5000, verifications: 5000, batch: true, batchMaxItems: 100, requestsPerMinute: 600, }, enterprise: { timestamps: null, verifications: null, batch: true, batchMaxItems: null, requestsPerMinute: 3000, }, }; log.info('config loaded', { veraeApiBaseUrl: config.veraeApiBaseUrl, mockVerae: config.mockVerae, natsEnabled: config.natsEnabled, natsUrl: config.natsUrl, port: config.port, });