← back to Instant Audit
server.js
199 lines
// Instant Website Audit — productized SDCC-generator report as a sellable
// digital product. TEST MODE ONLY. Runs locally; never deploys itself.
//
// HARD RAILS (inherited from AbramsEgo money-side pattern):
// - Stripe TEST keys only. sk_live_ is REFUSED at boot (money routes 503).
// A live key means Steve is flipping the switch himself — not us.
// - Absent key → checkout runs in MOCK mode: no real Stripe call, but the
// full fulfillment pipeline still fires so the flow is demoable end-to-end.
// - No real charges, no public exposure, no auto-send of the delivery email.
//
import 'dotenv/config';
import express from 'express';
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { fulfillOrder, TIER_SECTIONS } from './src/fulfill.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = parseInt(process.env.PORT || '9982', 10);
const DATA_DIR = path.join(__dirname, 'data');
const REPORTS_DIR = path.join(__dirname, 'reports');
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.mkdirSync(REPORTS_DIR, { recursive: true });
const ORDERS_LOG = path.join(DATA_DIR, 'orders.jsonl');
// ---- Stripe TEST-mode rail (mirrors AbramsEgo server.js) -------------------
const STRIPE_KEY = (process.env.STRIPE_SECRET_KEY || '').trim();
const STRIPE_WEBHOOK_SECRET = (process.env.STRIPE_WEBHOOK_SECRET || '').trim();
let stripe = null;
let stripeStatus = { configured: false, mode: 'mock', note: 'no test keys — MOCK checkout (fulfillment still runs)' };
if (STRIPE_KEY.startsWith('sk_live_')) {
console.error('!!! STRIPE_SECRET_KEY is a LIVE key (sk_live_). Instant Website Audit is TEST-mode only — REFUSING to boot money routes. Going live is Steve-gated; remove the live key.');
stripeStatus = { configured: false, mode: 'live-key-refused', note: 'LIVE key detected — money routes disabled (TEST mode only)' };
} else if (STRIPE_KEY.startsWith('sk_test_')) {
try {
const Stripe = (await import('stripe')).default;
stripe = new Stripe(STRIPE_KEY);
stripeStatus = { configured: true, mode: 'test', note: 'Stripe TEST mode — no real charges' };
} catch (e) { console.error('stripe init failed:', e.message); }
} else if (STRIPE_KEY) {
console.error('STRIPE_SECRET_KEY set but not sk_test_ — ignoring (TEST keys only).');
}
const TIERS = {
lite: { label: 'Audit Lite', usd: 49, blurb: 'Full site audit + a pack of working visitor tools.' },
standard: { label: 'Audit Standard', usd: 99, blurb: 'Everything in Lite + peer survey + trusted-sources hub.' },
pro: { label: 'Audit Pro', usd: 199, blurb: 'Everything + competitor teardown + hand-built homepage concepts.' },
};
const app = express();
// Webhook needs the RAW body — register BEFORE express.json().
app.post('/api/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
if (!stripe || !STRIPE_WEBHOOK_SECRET) return res.status(503).json({ error: 'stripe not configured' });
let event;
try {
event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], STRIPE_WEBHOOK_SECRET);
} catch (e) { return res.status(400).json({ error: `signature verification failed: ${e.message}` }); }
if (event.type === 'checkout.session.completed') {
const s = event.data.object;
const m = s.metadata || {};
await runFulfillment({ orderId: m.orderId || s.id, url: m.url, email: s.customer_email || m.email, tier: m.tier, baseUrl: baseUrlFrom(req) });
}
res.json({ received: true });
});
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
function baseUrlFrom(req) { return `${req.protocol}://${req.get('host')}`; }
// ---- sell config for the landing CTAs -------------------------------------
app.get('/api/sell/config', (req, res) => res.json({
mode: stripeStatus.mode,
checkoutReady: true, // mock mode is always "ready" locally
note: stripeStatus.note,
tiers: Object.entries(TIERS).map(([k, v]) => ({ key: k, ...v })),
}));
// ---- checkout: real Stripe TEST session, or MOCK when no keys --------------
app.post('/api/checkout', async (req, res) => {
const { tier, url, email } = req.body || {};
const t = TIERS[tier];
if (!t) return res.status(400).json({ error: `tier must be one of ${Object.keys(TIERS).join(', ')}` });
if (!url || !/^https?:\/\//i.test(url)) return res.status(400).json({ error: 'a valid http(s) website URL is required' });
const orderId = 'ord_' + crypto.randomBytes(6).toString('hex');
const base = baseUrlFrom(req);
if (stripe) {
// Real Stripe TEST-mode Checkout Session (one-time payment).
try {
const session = await stripe.checkout.sessions.create({
mode: 'payment',
customer_email: email || undefined,
line_items: [{
quantity: 1,
price_data: {
currency: 'usd',
unit_amount: t.usd * 100,
product_data: { name: `${t.label} — website audit`, description: t.blurb },
},
}],
metadata: { orderId, tier, url, email: email || '' },
success_url: `${base}/success?order=${orderId}&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${base}/?checkout=cancel`,
});
logOrder({ orderId, tier, url, email, mode: 'test', status: 'checkout_created', session: session.id });
return res.json({ ok: true, mode: 'test', url: session.url, orderId });
} catch (e) {
return res.status(502).json({ error: `stripe error: ${e.message}` });
}
}
// MOCK mode: no keys installed. Simulate a paid order and go straight to the
// success flow so the full pipeline is demoable. NO money moves.
logOrder({ orderId, tier, url, email, mode: 'mock', status: 'mock_paid' });
return res.json({ ok: true, mode: 'mock', url: `${base}/success?order=${orderId}&mock=1`, orderId });
});
// ---- success page: triggers fulfillment, shows delivery link ---------------
app.get('/success', async (req, res) => {
const orderId = String(req.query.order || '');
const mock = req.query.mock === '1';
const order = findOrder(orderId);
if (!order) return res.status(404).send(page('Order not found', '<p>We could not find that order.</p>'));
// In real Stripe mode fulfillment is driven by the webhook. But for a smooth
// local demo (and mock mode) we also fulfill here idempotently if not done.
let result = readManifest(orderId);
if (!result) result = await runFulfillment({ ...order, baseUrl: baseUrlFrom(req) });
const deliveryUrl = `/r/${orderId}/`;
const modeBadge = mock || order.mode === 'mock'
? '<span style="background:#fef3c7;border:1px solid #e4ddd2;padding:3px 8px;border-radius:3px;font-size:12px">MOCK ORDER · no charge</span>'
: '<span style="background:#e6f4ea;border:1px solid #cde;padding:3px 8px;border-radius:3px;font-size:12px">TEST payment · no real charge</span>';
res.send(page(`Your audit is ready`, `
${modeBadge}
<h1 style="margin-top:14px">Your ${TIERS[order.tier]?.label || order.tier} audit is ready</h1>
<p class="host">${result.manifest.host} · ${result.manifest.sections.length} sections</p>
<p><a class="cta" href="${deliveryUrl}">Open your report →</a></p>
<p style="font-size:14px;color:#6b6257">A delivery email has been <strong>drafted</strong> (not sent — sending is gated).
You can preview it <a href="/r/${orderId}/delivery-email.txt">here</a>.</p>
`));
});
// ---- serve the generated report bundles ------------------------------------
app.use('/r', express.static(REPORTS_DIR));
// ---- admin: list orders (local only) ---------------------------------------
app.get('/api/orders', (req, res) => res.json(readOrders()));
// ---- fulfillment runner (idempotent) ---------------------------------------
async function runFulfillment(order) {
const { orderId, url, email, tier, baseUrl } = order;
const existing = readManifest(orderId);
if (existing) return { manifest: existing };
const result = await fulfillOrder({ orderId, url, email, tier, baseUrl });
logOrder({ orderId, tier, url, email, status: 'fulfilled' });
console.log(`[fulfill] ${orderId} → ${result.deliveryUrl} (${result.files.length} files) $0 (local)`);
return result;
}
// ---- tiny order log helpers ------------------------------------------------
function logOrder(entry) {
fs.appendFileSync(ORDERS_LOG, JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n');
}
function readOrders() {
if (!fs.existsSync(ORDERS_LOG)) return [];
return fs.readFileSync(ORDERS_LOG, 'utf8').trim().split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
}
function findOrder(orderId) {
// Reconstruct the freshest view of an order from the log.
const rows = readOrders().filter((r) => r.orderId === orderId);
if (!rows.length) return null;
return Object.assign({}, ...rows);
}
function readManifest(orderId) {
const f = path.join(REPORTS_DIR, orderId, 'manifest.json');
if (!fs.existsSync(f)) return null;
try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; }
}
function page(title, body) {
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${title}</title><style>
body{font-family:-apple-system,Segoe UI,sans-serif;background:#faf8f4;color:#1a1a1a;line-height:1.6;max-width:640px;margin:0 auto;padding:60px 24px}
h1{font-family:Georgia,serif;font-size:30px}
.host{color:#6b6257}
.cta{display:inline-block;background:#1a1a1a;color:#fff;text-decoration:none;padding:12px 22px;border-radius:4px;font-size:15px;margin:10px 0}
.cta:hover{background:#8a7355}
a{color:#8a7355}
</style></head><body>${body}</body></html>`;
}
app.listen(PORT, '127.0.0.1', () => {
console.log(`Instant Website Audit on http://127.0.0.1:${PORT} [stripe: ${stripeStatus.mode}]`);
});