import "dotenv/config"; import path from "node:path"; const required = [ "ADMIN_SESSION_SECRET", "MERCHANT_SESSION_SECRET", "INTEGRATION_WEBHOOK_SECRET", "MQTT_BROKER_URL", "MQTT_USERNAME", "MQTT_PASSWORD", "MQTT_CLIENT_ID", "EXPORT_STORAGE_DIR" ]; const insecureDefaults = new Map([ ["ADMIN_TOKEN", "admin-dev-token"], ["MERCHANT_TOKEN", "merchant-dev-token"], ["DEVICE_TOKEN", "device-dev-token"], ["MERCHANT_PORTAL_PASSWORD", "merchant"], ["INTEGRATION_WEBHOOK_SECRET", "dev-callback-secret"], ["MQTT_PASSWORD", "change-me"], ["ADMIN_SESSION_SECRET", "change-me-long-random-admin-session-secret"], ["MERCHANT_SESSION_SECRET", "change-me-long-random-merchant-session-secret"] ]); const warnings = []; const errors = []; for (const name of required) { if (!process.env[name]) { errors.push(`${name} is required`); } } const pgVars = ["PGHOST", "PGPORT", "PGUSER", "PGPASSWORD", "PGDATABASE"]; const hasDatabaseUrl = Boolean(process.env.DATABASE_URL); const missingPgVars = pgVars.filter((name) => !process.env[name]); if (!hasDatabaseUrl && missingPgVars.length) { errors.push(`DATABASE_URL or ${pgVars.join("/")} is required`); } for (const [name, value] of insecureDefaults.entries()) { if (name === "ADMIN_TOKEN" && process.env.ADMIN_AUTH_ALLOW_LEGACY_TOKEN === "false") { continue; } if (name === "DEVICE_TOKEN" && process.env.DEVICE_AUTH_ALLOW_LEGACY_TOKEN === "false") { continue; } if (name === "MERCHANT_TOKEN" && process.env.MERCHANT_AUTH_ALLOW_LEGACY_TOKEN === "false") { continue; } if (name === "MERCHANT_PORTAL_PASSWORD" && process.env.MERCHANT_DEV_LOGIN_ENABLED === "false") { continue; } if (process.env[name] === value) { errors.push(`${name} still uses the development default`); } } function requireFalse(name) { if (process.env[name] !== "false") { errors.push(`${name} must be false in production`); } } function requireTrue(name, message) { if (process.env[name] !== "true") { errors.push(message || `${name} must be true in production`); } } requireFalse("ADMIN_AUTH_ALLOW_LEGACY_TOKEN"); requireFalse("DEVICE_AUTH_ALLOW_LEGACY_TOKEN"); requireFalse("MERCHANT_AUTH_ALLOW_LEGACY_TOKEN"); if (process.env.ADMIN_DEV_LOGIN_ENABLED !== "false") { errors.push("ADMIN_DEV_LOGIN_ENABLED must be false in production"); } if (process.env.MERCHANT_DEV_LOGIN_ENABLED !== "false") { errors.push("MERCHANT_DEV_LOGIN_ENABLED must be false in production"); } if (process.env.MQTT_PUBLISH_MODE !== "broker") { errors.push("MQTT_PUBLISH_MODE must be broker in production"); } if (process.env.MQTT_SUBSCRIBE_ENABLED !== "true") { warnings.push("MQTT_SUBSCRIBE_ENABLED should be true when device uplink observability is required"); } requireTrue("SETTLEMENT_ADJUSTMENT_REQUIRE_APPROVAL", "SETTLEMENT_ADJUSTMENT_REQUIRE_APPROVAL must be true for production finance control"); if (process.env.DATABASE_URL && !/^postgres(ql)?:\/\//.test(process.env.DATABASE_URL)) { errors.push("DATABASE_URL must be a postgres connection string"); } if (!process.env.DATABASE_URL && process.env.PGPORT && !Number.isInteger(Number(process.env.PGPORT))) { errors.push("PGPORT must be a number"); } if (process.env.MQTT_BROKER_URL && !/^mqtts:\/\//.test(process.env.MQTT_BROKER_URL)) { warnings.push("MQTT_BROKER_URL should use mqtts:// for production"); } for (const name of ["ADMIN_SESSION_SECRET", "MERCHANT_SESSION_SECRET", "INTEGRATION_WEBHOOK_SECRET", "MQTT_PASSWORD"]) { if ((process.env[name] || "").length < 24) { errors.push(`${name} must be at least 24 characters`); } } if (process.env.LOG_FORMAT !== "json") { warnings.push("LOG_FORMAT should be json in production"); } if (process.env.EXPORT_WORKER_ENABLED !== "true") { errors.push("EXPORT_WORKER_ENABLED must be true in production"); } if (Number(process.env.EXPORT_RETENTION_DAYS || 0) <= 0) { errors.push("EXPORT_RETENTION_DAYS must be a positive number"); } if (process.env.EXPORT_STORAGE_DIR && !path.isAbsolute(process.env.EXPORT_STORAGE_DIR)) { errors.push("EXPORT_STORAGE_DIR must be an absolute path in production"); } if (process.env.RATE_LIMIT_ENABLED !== "true") { errors.push("RATE_LIMIT_ENABLED must be true in production"); } if (process.env.TRUST_PROXY !== "true") { warnings.push("TRUST_PROXY should be true when running behind a reverse proxy/load balancer"); } if (!process.env.JSON_BODY_LIMIT) { errors.push("JSON_BODY_LIMIT is required"); } for (const warning of warnings) { console.warn(`WARN ${warning}`); } if (errors.length) { for (const error of errors) { console.error(`ERROR ${error}`); } process.exit(1); } console.log("Production environment preflight passed");