← back to Costa Rica

lib/preflight.js

73 lines

'use strict';
// TK-10346 — boot-time fail-closed guard against a silent revenue-loss misconfig.
//
// If a payment/WhatsApp integration is LIVE but its webhook secret is missing,
// verifyWebhook returns { ok: false } for EVERY real webhook -> the charge succeeds
// at the processor but our booking never transitions to confirmed. Payments happen,
// bookings don't, and nothing errors loudly. This catches it at boot.
//
// In production this THROWS (a live money system that cannot verify webhooks must not
// serve — fail closed). In dev/sandbox it only warns. It is INERT when nothing is live,
// so it can never affect the current sandbox prod.
//
// SCOPE: this guards the webhook-secret / verify-token misconfig ONLY. It does NOT prove
// the live charge path is correct — the go-live memo's LIVE-ONLY preflight (createCharge
// providerRef=undefined guard, base64-vs-hex signature encoding) must still be checked
// against the real provider. "Boot passed" != "safe to open bookings".

const SECRET_ENV = { tilopay: 'TILOPAY_WEBHOOK_SECRET', onvo: 'ONVO_WEBHOOK_SECRET' };

// Pure — returns an array of human-readable problem strings (empty = all good).
function checkWebhookSecrets({ getProvider, whatsapp } = {}) {
  const problems = [];
  if (getProvider) {
    try {
      const pay = getProvider();
      if (pay && pay.liveMode && !pay.webhookSecretSet) {
        const envName = SECRET_ENV[pay.name] || `${String(pay.name).toUpperCase()}_WEBHOOK_SECRET`;
        problems.push(
          `payment provider "${pay.name}" is LIVE but its webhook secret is missing ` +
          `(set ${envName}) — real payment webhooks would be silently rejected and bookings never confirm.`
        );
      }
    } catch (e) {
      problems.push(`payment provider preflight could not resolve: ${e && e.message}`);
    }
  }
  if (whatsapp && whatsapp.liveMode) {
    if (!whatsapp.webhookSecretSet) {
      problems.push(
        `WhatsApp is LIVE but WHATSAPP_APP_SECRET is missing — inbound webhooks would be silently rejected.`
      );
    }
    if (!whatsapp.verifyTokenSet) {
      problems.push(
        `WhatsApp is LIVE but WHATSAPP_VERIFY_TOKEN is missing or left at the public default ` +
        `('cr-verify-sandbox') — set a real random token before registering the Meta webhook.`
      );
    }
  }
  return problems;
}

// Side-effecting boot gate. Logs a loud banner; throws in production.
function runPreflight(deps = {}) {
  const env = deps.env || process.env;
  const problems = checkWebhookSecrets(deps);
  if (problems.length) {
    const line = '═'.repeat(79);
    console.error(
      '\n' + line +
      '\nFATAL PREFLIGHT — live integration without webhook verification:\n' +
      problems.map((p) => '  • ' + p).join('\n') +
      '\n' + line + '\n'
    );
    if ((env.NODE_ENV || '') === 'production') {
      throw new Error('preflight failed: ' + problems.join(' | '));
    }
  }
  return problems;
}

module.exports = { checkWebhookSecrets, runPreflight };