← back to NationalPaperHangers

scripts/go-live-check.js

96 lines

#!/usr/bin/env node
// Pre-flight env-var checklist for going live.
// Reads each env var the app expects, prints PRESENT/ABSENT, and (when
// NODE_ENV=production) exits non-zero if any required-for-live var is absent.

require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });

// requirement: 'live' | 'optional' | 'always'
//   live  — must be present when NODE_ENV=production
//   always — must be present in every env (dev + prod)
//   optional — feature flag; absence just disables a thing
const VARS = [
  // Core
  { name: 'NODE_ENV',                 req: 'always',   desc: 'Environment marker. Set to "production" for live.' },
  { name: 'PORT',                     req: 'always',   desc: 'HTTP port. Defaults to 9765.' },
  { name: 'PUBLIC_URL',               req: 'live',     desc: 'Canonical public URL. Used for emails, OG, callback URLs.' },

  // Database
  { name: 'PGHOST',                   req: 'always',   desc: 'Postgres host.' },
  { name: 'PGPORT',                   req: 'optional', desc: 'Postgres port (default 5432).' },
  { name: 'PGDATABASE',               req: 'always',   desc: 'Postgres DB name (national_paper_hangers).' },
  { name: 'PGUSER',                   req: 'always',   desc: 'Postgres user.' },
  { name: 'PGPASSWORD',               req: 'optional', desc: 'Postgres password (omit for unix-socket trust).' },

  // Sessions / signing
  { name: 'SESSION_SECRET',           req: 'live',     desc: 'Cookie session signing secret.' },
  { name: 'BOOKING_SIGNING_SECRET',   req: 'live',     desc: 'HMAC for /bookings/:uuid?t=… view tokens.' },
  { name: 'UNSUBSCRIBE_SIGNING_SECRET', req: 'live',   desc: 'HMAC for one-click unsubscribe links.' },

  // Stripe — subscriptions + booking deposits + Connect
  { name: 'STRIPE_SECRET_KEY',        req: 'live',     desc: 'sk_live_… (or sk_test_… in staging). Drives all Stripe SDK calls.' },
  { name: 'STRIPE_PUBLISHABLE_KEY',   req: 'live',     desc: 'pk_live_… exposed to book.ejs for Stripe Elements.' },
  { name: 'STRIPE_WEBHOOK_SECRET',    req: 'live',     desc: 'whsec_… for /webhooks/stripe signature verify.' },
  { name: 'STRIPE_DEV_ACCEPT_UNSIGNED', req: 'optional', desc: 'Dev escape hatch — only honored when NODE_ENV != production.' },
  { name: 'STRIPE_PRICE_PRO_MONTH',   req: 'live',     desc: 'Stripe price ID for Pro monthly subscription.' },
  { name: 'STRIPE_PRICE_PRO_YEAR',    req: 'live',     desc: 'Stripe price ID for Pro annual subscription.' },
  { name: 'STRIPE_PRICE_SIGNATURE_MONTH', req: 'live', desc: 'Stripe price ID for Signature monthly.' },
  { name: 'STRIPE_PRICE_SIGNATURE_YEAR',  req: 'live', desc: 'Stripe price ID for Signature annual.' },
  { name: 'STRIPE_PRICE_ENTERPRISE_MONTH', req: 'live',desc: 'Stripe price ID for Enterprise monthly.' },

  // Marketplace tunables
  { name: 'NPH_DEFAULT_DEPOSIT_CENTS', req: 'optional', desc: 'Default booking deposit in cents (default 9900 = $99).' },
  { name: 'NPH_PLATFORM_FEE_BPS',      req: 'optional', desc: 'Platform fee in basis points (default 1000 = 10%).' },
  { name: 'NPH_PLATFORM_ADMIN_INSTALLER_IDS', req: 'optional', desc: 'Comma-separated installer IDs that see platform-wide totals on /admin/billing.' },

  // George (outbound email)
  { name: 'GEORGE_URL',               req: 'live',     desc: 'George Gmail relay URL (http://localhost:9850 in dev, http://100.107.67.67:9850 from Kamatera tailnet).' },
  { name: 'GEORGE_USER',              req: 'live',     desc: 'George basic-auth user.' },
  { name: 'GEORGE_PASS',              req: 'live',     desc: 'George basic-auth password.' },
  { name: 'GEORGE_ACCOUNT',           req: 'optional', desc: 'Account label for George multi-account support (e.g. info).' },
  { name: 'EMAIL_FROM',               req: 'live',     desc: 'From address for outbound mail (e.g. info@nationalpaperhangers.com).' },
  { name: 'EMAIL_FROM_NAME',          req: 'optional', desc: 'From display name (e.g. National Paper Hangers).' },
  { name: 'MAILING_ADDRESS',          req: 'live',     desc: 'CAN-SPAM physical mailing address. Pre-flight gate REFUSES sends without this.' },

  // Optional integrations
  { name: 'BROWSERBASE_API_KEY',      req: 'optional', desc: 'Cloud browser for vendor scrapes.' },
  { name: 'BROWSERBASE_PROJECT_ID',   req: 'optional', desc: 'Browserbase project ID.' }
];

const isProd = process.env.NODE_ENV === 'production';
const rows = VARS.map(v => ({
  ...v,
  present: typeof process.env[v.name] === 'string' && process.env[v.name].length > 0
}));

function pad(s, n) { return (s + ' '.repeat(n)).slice(0, n); }
const NAME_W = Math.max(...rows.map(r => r.name.length)) + 2;

console.log('');
console.log(`NPH go-live env-var checklist (NODE_ENV=${process.env.NODE_ENV || '<unset>'})`);
console.log('='.repeat(72));
let missingLive = 0, missingAlways = 0;
for (const r of rows) {
  const tag = r.present ? '✓ PRESENT' : '✗ ABSENT ';
  const reqTag = r.req === 'always' ? '[always]' : r.req === 'live' ? '[live]  ' : '[opt]   ';
  console.log(`  ${tag}  ${reqTag}  ${pad(r.name, NAME_W)} ${r.desc}`);
  if (!r.present) {
    if (r.req === 'always') missingAlways += 1;
    if (r.req === 'live') missingLive += 1;
  }
}
console.log('');
console.log(`Required-always missing: ${missingAlways}`);
console.log(`Required-for-live missing: ${missingLive}`);

if (missingAlways > 0) {
  console.error('\n✗ FAIL — required-always env vars are missing. Fix .env before any boot.');
  process.exit(2);
}
if (isProd && missingLive > 0) {
  console.error('\n✗ FAIL — running in production but required-for-live env vars are absent. Save them via the secrets-manager skill.');
  process.exit(1);
}
console.log('\n✓ OK for current NODE_ENV. (live keys absent is fine in dev.)');
process.exit(0);