#!/usr/bin/env node import { execSync } from "node:child_process"; import { env } from "node:process"; const requiredVariables = [ { key: "DATABASE_URL", requiredInProduction: true }, { key: "AUTH_SECRET", requiredInProduction: true }, { key: "CAMPAIGN_RETRY_JOB_TOKEN", requiredInProduction: true }, { key: "WHATSAPP_WEBHOOK_VERIFY_TOKEN", requiredInProduction: false }, { key: "WHATSAPP_WEBHOOK_SECRET", requiredInProduction: false }, { key: "NEXT_PUBLIC_APP_URL", requiredInProduction: false }, { key: "APP_URL", requiredInProduction: false }, { key: "OPS_BASE_URL", requiredInProduction: false } ]; const placeholderPatterns = [ "change-me", "changeme", "your-", "example", "todo", "placeholder" ]; const isProductionLike = env.NODE_ENV === "production"; let hasFailure = false; function isPresent(value) { return typeof value === "string" && value.trim().length > 0; } function isPlaceholderValue(value) { const lowered = value.toLowerCase(); return placeholderPatterns.some((pattern) => lowered.includes(pattern)); } function validateSecretValue(key, value, expected) { if (!isPresent(value)) { if (expected) { reportIssue("error", `${key} is missing`); } return; } if (expected && value === expected) { reportIssue("error", `${key} is using production default placeholder`); } if (isPlaceholderValue(value)) { reportIssue("warn", `${key} looks like placeholder value`); } } function reportIssue(level, message) { if (level === "error") { console.error(`[ERROR] ${message}`); hasFailure = true; return; } if (level === "warn") { console.warn(`[WARN] ${message}`); return; } console.log(`[OK] ${message}`); } function checkEnv() { console.log("Checking environment variables..."); for (const item of requiredVariables) { const value = env[item.key]; const isRequired = item.requiredInProduction && isProductionLike; if (!isPresent(value)) { if (isRequired) { reportIssue("error", `${item.key} is missing`); } else { reportIssue("warn", `${item.key} is missing (optional)`); } continue; } if (item.key === "AUTH_SECRET") { validateSecretValue(item.key, value, "whatsapp-inbox-dev-secret"); continue; } if (isPlaceholderValue(value)) { reportIssue("warn", `${item.key} has placeholder-like value`); } const display = value.replace(/./g, "*").slice(0, 4); reportIssue("ok", `${item.key} set (${display}...)`); } } function checkPrismaMigrationStatus() { console.log("Checking Prisma migration status..."); try { const output = execSync("npx prisma migrate status --schema prisma/schema.prisma", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }); if (/database is up to date/i.test(output)) { reportIssue("ok", "Database schema is up to date"); return; } if (/No migration found/i.test(output)) { reportIssue("warn", "No migration found for current database"); return; } reportIssue("warn", `Prisma migrate status output: ${output.trim()}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); reportIssue("error", `Prisma migration check failed: ${message}`); } } async function runHealthCheck() { const baseUrl = (env.OPS_BASE_URL || env.APP_URL || env.NEXT_PUBLIC_APP_URL || "http://localhost:3000").replace(/\/+$/, ""); const healthToken = env.HEALTHCHECK_TOKEN?.trim(); const url = `${baseUrl}/api/health`; const headers = healthToken ? { Authorization: `Bearer ${healthToken}` } : {}; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 12_000); try { const response = await fetch(url, { headers, signal: controller.signal }); clearTimeout(timeout); if (!response.ok) { reportIssue("error", `Health endpoint returned ${response.status}`); return; } const payload = await response.json(); if (!payload?.ok) { reportIssue("warn", `Health endpoint warning: ${JSON.stringify(payload)}`); } else { reportIssue("ok", `Health endpoint OK: ${payload.status}`); } } catch (error) { clearTimeout(timeout); reportIssue("error", `Health endpoint unreachable: ${error instanceof Error ? error.message : String(error)}`); } } async function runRetryEndpointCheck() { const baseUrl = (env.OPS_BASE_URL || env.APP_URL || env.NEXT_PUBLIC_APP_URL || "http://localhost:3000").replace(/\/+$/, ""); const token = env.CAMPAIGN_RETRY_JOB_TOKEN?.trim(); if (!token) { reportIssue("warn", "CAMPAIGN_RETRY_JOB_TOKEN missing; campaign job endpoint health check skipped"); return; } const url = `${baseUrl}/api/jobs/campaign-retry?token=${encodeURIComponent(token)}`; const headers = { Authorization: `Bearer ${token}` }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 12_000); try { const response = await fetch(url, { headers, signal: controller.signal }); clearTimeout(timeout); if (!response.ok) { reportIssue("warn", `Campaign retry endpoint returned ${response.status}`); return; } const payload = await response.json(); reportIssue("ok", `Campaign retry endpoint reachable: ${payload.ok ? "ok" : "down"}`); } catch (error) { clearTimeout(timeout); reportIssue("warn", `Campaign retry endpoint error: ${error instanceof Error ? error.message : String(error)}`); } } async function main() { checkEnv(); checkPrismaMigrationStatus(); await runHealthCheck(); await runRetryEndpointCheck(); if (hasFailure) { reportIssue("error", "Readiness check failed."); process.exit(1); } reportIssue("ok", "Readiness check completed."); } await main();