#!/usr/bin/env node import { readFile } from "node:fs/promises"; import path from "node:path"; const targetPath = path.resolve(process.cwd(), process.argv[2] || ".env.production"); const requiredKeys = [ "PUBLIC_DOMAIN", "PUBLIC_URL", "TLS_EMAIL", "BETTER_AUTH_SECRET", "MAIL_ENCRYPTION_KEY", "CORS_ALLOW_ORIGINS", "AUTH_MAGIC_LINK_PROVIDER", "AUTH_MAIL_FROM", "AUTH_SMTP_HOST", "AUTH_SMTP_PORT", "AUTH_SMTP_SECURE", "AUTH_SMTP_USER", "AUTH_SMTP_PASSWORD", "POSTGRES_PASSWORD", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY", "S3_SECRET_KEY" ]; const secretKeys = [ "BETTER_AUTH_SECRET", "MAIL_ENCRYPTION_KEY", "AUTH_SMTP_PASSWORD", "POSTGRES_PASSWORD", "S3_ACCESS_KEY", "S3_SECRET_KEY" ]; const optionalSecretKeys = [ "METRICS_AUTH_TOKEN" ]; const insecureSecretValues = new Set([ "", "changeme", "change-me", "replace-me", "replace-with-a-long-random-secret", "replace-with-a-different-long-random-secret", "replace-with-smtp-password", "replace-with-strong-password", "replace-with-access-key", "replace-with-secret-key", "replace-with-metrics-token" ]); function stripOptionalQuotes(value) { const trimmed = value.trim(); if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { return trimmed.slice(1, -1); } return trimmed; } function parseEnv(raw) { const result = new Map(); for (const line of raw.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { continue; } const normalized = trimmed.startsWith("export ") ? trimmed.slice("export ".length).trim() : trimmed; const equalsIndex = normalized.indexOf("="); if (equalsIndex <= 0) { continue; } const key = normalized.slice(0, equalsIndex).trim(); const value = stripOptionalQuotes(normalized.slice(equalsIndex + 1)); result.set(key, value); } return result; } function isLocalHost(hostname) { const normalized = String(hostname || "").toLowerCase(); return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1"; } function parseAbsoluteURL(value, envName, errors) { try { const parsed = new URL(value); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { errors.push(`${envName} must use http or https`); return null; } return parsed; } catch { errors.push(`${envName} must be a valid absolute URL`); return null; } } function isLikelyEmail(value) { const normalized = String(value || "").trim(); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized); } function parseBoolean(value) { const normalized = String(value || "").trim().toLowerCase(); if (normalized === "true" || normalized === "1" || normalized === "yes") { return true; } if (normalized === "false" || normalized === "0" || normalized === "no") { return false; } return null; } function normalizeOrigin(url) { return `${url.protocol}//${url.host}`.toLowerCase(); } async function main() { const errors = []; const warnings = []; let raw; try { raw = await readFile(targetPath, "utf8"); } catch (error) { console.error(`[error] failed to read ${targetPath}: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } const env = parseEnv(raw); for (const key of requiredKeys) { const value = env.get(key); if (!value || !value.trim()) { errors.push(`${key} is required`); } } for (const key of secretKeys) { const value = String(env.get(key) || "").trim(); const normalized = value.toLowerCase(); if (insecureSecretValues.has(normalized) || value.length < 16) { errors.push(`${key} must be a strong non-placeholder secret (minimum 16 characters)`); } } for (const key of optionalSecretKeys) { const value = String(env.get(key) || "").trim(); if (!value) { continue; } const normalized = value.toLowerCase(); if (insecureSecretValues.has(normalized) || value.length < 16) { errors.push(`${key} must be a strong non-placeholder secret (minimum 16 characters) when set`); } } const publicURL = env.get("PUBLIC_URL"); const publicDomain = String(env.get("PUBLIC_DOMAIN") || "").trim().toLowerCase(); let normalizedPublicOrigin = ""; if (publicURL) { const parsedPublicURL = parseAbsoluteURL(publicURL, "PUBLIC_URL", errors); if (parsedPublicURL) { normalizedPublicOrigin = normalizeOrigin(parsedPublicURL); if (!isLocalHost(parsedPublicURL.hostname) && parsedPublicURL.protocol !== "https:") { errors.push("PUBLIC_URL must use https for non-local deployments"); } if (publicDomain && parsedPublicURL.hostname.toLowerCase() !== publicDomain) { errors.push(`PUBLIC_URL host (${parsedPublicURL.hostname}) must match PUBLIC_DOMAIN (${publicDomain})`); } } } const corsOrigins = env.get("CORS_ALLOW_ORIGINS"); if (corsOrigins) { if (corsOrigins.includes("*")) { errors.push("CORS_ALLOW_ORIGINS cannot include '*'"); } const normalizedCorsOrigins = new Set(); for (const rawOrigin of corsOrigins.split(",")) { const origin = rawOrigin.trim(); if (!origin) { continue; } const parsedOrigin = parseAbsoluteURL(origin, "CORS_ALLOW_ORIGINS", errors); if (!parsedOrigin) { continue; } if ((parsedOrigin.pathname && parsedOrigin.pathname !== "/") || parsedOrigin.search || parsedOrigin.hash) { errors.push(`CORS_ALLOW_ORIGINS origin must not include path/query/fragment: ${origin}`); } if (!isLocalHost(parsedOrigin.hostname) && parsedOrigin.protocol !== "https:") { errors.push(`CORS_ALLOW_ORIGINS origin must use https for non-local deployments: ${origin}`); } if (publicDomain !== "localhost" && isLocalHost(parsedOrigin.hostname)) { errors.push(`CORS_ALLOW_ORIGINS cannot include localhost in production: ${origin}`); } normalizedCorsOrigins.add(normalizeOrigin(parsedOrigin)); } if (normalizedPublicOrigin && !normalizedCorsOrigins.has(normalizedPublicOrigin)) { errors.push(`CORS_ALLOW_ORIGINS must include PUBLIC_URL origin (${normalizedPublicOrigin})`); } } const magicLinkProvider = String(env.get("AUTH_MAGIC_LINK_PROVIDER") || "").trim().toLowerCase(); if (magicLinkProvider !== "smtp") { errors.push("AUTH_MAGIC_LINK_PROVIDER must be smtp for production deployments"); } const authDevMailboxEnabled = String(env.get("AUTH_DEV_MAILBOX_ENABLED") || "").trim().toLowerCase(); if (authDevMailboxEnabled === "true" || authDevMailboxEnabled === "1" || authDevMailboxEnabled === "yes") { errors.push("AUTH_DEV_MAILBOX_ENABLED must be false in production"); } const smtpPort = Number.parseInt(String(env.get("AUTH_SMTP_PORT") || ""), 10); if (Number.isNaN(smtpPort) || smtpPort < 1 || smtpPort > 65535) { errors.push("AUTH_SMTP_PORT must be a valid TCP port"); } const smtpSecure = parseBoolean(env.get("AUTH_SMTP_SECURE")); if (smtpSecure === null) { errors.push("AUTH_SMTP_SECURE must be true/false"); } else if (smtpSecure === false && smtpPort === 465) { errors.push("AUTH_SMTP_SECURE should be true when AUTH_SMTP_PORT=465"); } const smtpSkipVerify = parseBoolean(env.get("AUTH_SMTP_SKIP_VERIFY")); if (smtpSkipVerify === true) { errors.push("AUTH_SMTP_SKIP_VERIFY must not be enabled in production"); } const smtpRejectUnauthorized = parseBoolean(env.get("AUTH_SMTP_TLS_REJECT_UNAUTHORIZED")); if (smtpRejectUnauthorized === false) { errors.push("AUTH_SMTP_TLS_REJECT_UNAUTHORIZED must not be false in production"); } const tlsEmail = String(env.get("TLS_EMAIL") || "").trim(); if (!isLikelyEmail(tlsEmail)) { errors.push("TLS_EMAIL must be a valid email address for ACME certificate registration"); } const authMailFrom = String(env.get("AUTH_MAIL_FROM") || "").trim(); if (!isLikelyEmail(authMailFrom)) { errors.push("AUTH_MAIL_FROM must be a valid sender email address"); } if (String(env.get("BETTER_AUTH_SECRET") || "") === String(env.get("MAIL_ENCRYPTION_KEY") || "")) { errors.push("BETTER_AUTH_SECRET and MAIL_ENCRYPTION_KEY must be different secrets"); } if (publicDomain === "localhost") { warnings.push("PUBLIC_DOMAIN is localhost; use a real domain for external production traffic."); } if (warnings.length) { 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(`[ok] ${targetPath} passed production environment validation.`); } void main();