152 lines
4.6 KiB
JavaScript
152 lines
4.6 KiB
JavaScript
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");
|