Milestone 0: import zappier billing, Verae middleware, and Zapier research

Compose-ready workspace: packages/zappier (rate card, portal, Stripe),
packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier
(CLI app), vendor/zapier-platform, and research/zapier vendor corpus.

Gate 0 structure checks pass. Product code and research are not yet wired.
This commit is contained in:
George Lambert 2026-09-09 02:37:36 -04:00
commit b4150c8250
1364 changed files with 6814366 additions and 0 deletions

View file

@ -0,0 +1,485 @@
import { randomBytes, randomUUID } from 'crypto';
import fs from 'fs';
import path from 'path';
import { RequestHandler, Router } from 'express';
import { verifyPassword } from './accounts';
import { AdminUserRepo, makeAdminUser } from './admin-users';
import { CustomerRepo } from './auth';
import { buildInvoice, Invoice, InvoiceRepo } from './invoicing';
import { PriceRule, PricingStore, TierConfig } from './pricing';
import { PROJECT_ROOT } from './paths';
import { billingRows, toCsv, usageTrend } from './reports';
import { UsageRepo } from './usage';
// Issued login tokens (in-memory; a restart simply requires logging in again).
const sessions = new Map<string, number>();
export function adminLoginRouter(users: AdminUserRepo): Router {
const router = Router();
router.post('/login', (req, res) => {
const { username, password } = req.body ?? {};
if (typeof username !== 'string' || typeof password !== 'string') {
res.status(401).json({ error: 'invalid username or password' });
return;
}
const user = users.findByUsername(username);
if (!user || !user.active || !verifyPassword(password, user.passwordHash)) {
res.status(401).json({ error: 'invalid username or password' });
return;
}
const token = randomBytes(24).toString('hex');
sessions.set(token, Date.now());
res.json({ token });
});
return router;
}
export function adminAuth(): RequestHandler {
return (req, res, next) => {
const expected = process.env.ADMIN_KEY ?? 'admin-dev-key';
if (req.header('x-admin-key') === expected) return next();
const bearer = req.header('authorization');
const token = bearer?.startsWith('Bearer ') ? bearer.slice(7) : undefined;
if (token && sessions.has(token)) return next();
res.status(403).json({ error: 'invalid or missing admin key' });
};
}
function isValidRule(rule: unknown): rule is PriceRule {
if (!rule || typeof rule !== 'object') return false;
const r = rule as Record<string, unknown>;
if (r.kind === 'free') return true;
if (r.kind === 'fixed') return typeof r.fixedCents === 'number';
if (r.kind === 'variable') {
return ['baseCents', 'perKbCents', 'perMbCents'].every((k) => typeof r[k] === 'number');
}
return false;
}
function isValidTier(tier: unknown): tier is TierConfig {
if (!tier || typeof tier !== 'object') return false;
const t = tier as Record<string, unknown>;
return (
typeof t.name === 'string' &&
typeof t.multiplier === 'number' &&
typeof t.monthlyCreditCents === 'number' &&
(t.defaultRule === undefined || isValidRule(t.defaultRule))
);
}
export interface AccountingDeps {
usage: UsageRepo;
invoices: InvoiceRepo;
users: AdminUserRepo;
}
const PERIOD_RE = /^\d{4}-(0[1-9]|1[0-2])$/;
function periodWindow(period: string): { from: Date; to: Date } {
const [y, m] = period.split('-').map(Number);
return { from: new Date(Date.UTC(y, m - 1, 1)), to: new Date(Date.UTC(y, m, 1)) };
}
function parseDate(value: unknown): Date | undefined {
if (typeof value !== 'string' || value.trim() === '') return undefined;
const d = new Date(value);
return Number.isNaN(d.getTime()) ? undefined : d;
}
function escapeHtml(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
const dollars = (cents: number): string => `$${(cents / 100).toFixed(2)}`;
export function renderInvoiceHtml(invoice: Invoice, customerName: string): string {
const rows = invoice.lines
.map(
(l) =>
`<tr><td>${escapeHtml(l.endpointId)}</td><td>${l.calls}</td><td>${dollars(l.cents)}</td></tr>`,
)
.join('');
const po = invoice.poNumber
? `<p><strong>Purchase order:</strong> ${escapeHtml(invoice.poNumber)}</p>`
: '';
const due = invoice.dueAtMs ? new Date(invoice.dueAtMs).toISOString().slice(0, 10) : '—';
return `<!doctype html><html><head><meta charset="utf-8"><title>${invoice.id}</title>
<style>
body { font-family: -apple-system, "Segoe UI", sans-serif; max-width: 720px; margin: 2rem auto; color: #171a26; }
h1 { font-size: 1.4rem; margin-bottom: 0; }
.muted { color: #6b7186; }
table { border-collapse: collapse; width: 100%; margin: 1.5rem 0; }
th, td { border-bottom: 1px solid #e5e7f0; text-align: left; padding: 0.5rem 0.6rem; }
th { font-size: 0.75rem; text-transform: uppercase; color: #6b7186; }
.totals td { border: 0; padding: 0.2rem 0.6rem; }
.totals .grand { font-weight: 700; font-size: 1.1rem; border-top: 2px solid #171a26; }
.status { display: inline-block; padding: 0.15rem 0.7rem; border-radius: 999px; background: #eef0fe; color: #4f46e5; font-weight: 700; font-size: 0.8rem; }
@media print { body { margin: 0; } }
</style></head><body>
<h1>Invoice ${escapeHtml(invoice.id)}</h1>
<p class="muted">Zappier API usage · period ${escapeHtml(invoice.period)} · due ${due}</p>
<p><span class="status">${invoice.status.toUpperCase()}</span></p>
<p><strong>Billed to:</strong> ${escapeHtml(customerName)} (${escapeHtml(invoice.customerId)})<br>
<strong>Billing type:</strong> ${escapeHtml(invoice.billingType)}</p>
${po}
<table><thead><tr><th>Endpoint</th><th>Calls</th><th>Amount</th></tr></thead><tbody>${rows}</tbody></table>
<table class="totals">
<tr><td>Usage total</td><td>${dollars(invoice.totalCents)}</td></tr>
<tr><td>Monthly credit</td><td>${dollars(invoice.creditCents)}</td></tr>
<tr class="grand"><td>Amount due</td><td>${dollars(invoice.billableCents)}</td></tr>
</table>
</body></html>`;
}
export function adminRouter(
store: PricingStore,
customers: CustomerRepo,
accounting: AccountingDeps,
): Router {
const router = Router();
router.get('/pricing', (req, res) => {
res.json({ rateCard: store.getRateCard(), tiers: store.getTiers() });
});
router.put('/endpoints/:id', (req, res) => {
if (!isValidRule(req.body)) {
res.status(400).json({ error: 'invalid price rule' });
return;
}
store.upsertEndpoint(req.params.id, req.body);
res.json({ ok: true });
});
router.delete('/endpoints/:id', (req, res) => {
store.deleteEndpoint(req.params.id);
res.json({ ok: true });
});
router.put('/tiers/:id', (req, res) => {
if (!isValidTier(req.body)) {
res.status(400).json({ error: 'invalid tier config' });
return;
}
store.upsertTier({ ...req.body, id: req.params.id });
res.json({ ok: true });
});
router.delete('/tiers/:id', (req, res) => {
store.deleteTier(req.params.id);
res.json({ ok: true });
});
router.get('/customers', (req, res) => {
res.json({ customers: customers.list().map((c) => ({ ...c, apiKey: undefined })) });
});
router.post('/customers', (req, res) => {
const { name, tierId } = req.body ?? {};
if (typeof name !== 'string' || typeof tierId !== 'string') {
res.status(400).json({ error: 'name and tierId required' });
return;
}
const customer = {
id: `cust_${randomUUID().slice(0, 8)}`,
name,
tierId,
apiKey: `key-${randomBytes(12).toString('hex')}`,
};
customers.save(customer);
res.status(201).json(customer);
});
router.put('/customers/:id', (req, res) => {
const existing = customers.list().find((c) => c.id === req.params.id);
if (!existing) {
res.status(404).json({ error: 'customer not found' });
return;
}
const { name, tierId, multiplierOverride, stripeCustomerId, billingType, email } =
req.body ?? {};
if (
billingType !== undefined &&
billingType !== 'stripe' &&
billingType !== 'purchase_order'
) {
res.status(400).json({ error: 'billingType must be stripe or purchase_order' });
return;
}
customers.save({
...existing,
...(name !== undefined ? { name } : {}),
...(tierId !== undefined ? { tierId } : {}),
...(multiplierOverride !== undefined ? { multiplierOverride } : {}),
...(stripeCustomerId !== undefined ? { stripeCustomerId } : {}),
...(billingType !== undefined ? { billingType } : {}),
...(email !== undefined ? { email } : {}),
});
res.json({ ok: true });
});
/* ---------------- accounting: invoices ---------------- */
router.post('/invoices/generate', (req, res) => {
const { period, customerId, poNumber } = req.body ?? {};
if (typeof period !== 'string' || !PERIOD_RE.test(period)) {
res.status(400).json({ error: 'period must be YYYY-MM' });
return;
}
const { from, to } = periodWindow(period);
const tiers = store.getTiers();
const generated: string[] = [];
const skipped: { customerId: string; reason: string }[] = [];
for (const customer of customers.list()) {
if (customerId && customer.id !== customerId) continue;
const existing = accounting.invoices
.list({ customerId: customer.id, period })
.find((i) => i.status !== 'draft');
if (existing) {
skipped.push({ customerId: customer.id, reason: `${existing.id} already ${existing.status}` });
continue;
}
const entries = accounting.usage
.listFor(customer.id, from)
.filter((e) => e.timestamp < to);
if (entries.length === 0) {
skipped.push({ customerId: customer.id, reason: 'no usage in period' });
continue;
}
const tier = tiers.find((t) => t.id === customer.tierId);
if (!tier) {
skipped.push({ customerId: customer.id, reason: `unknown tier ${customer.tierId}` });
continue;
}
const draft = accounting.invoices.list({ customerId: customer.id, period })[0];
const invoice = buildInvoice({
customer,
period,
sequence: draft ? Number(draft.id.slice(-4)) : accounting.invoices.nextSequence(period),
entries,
tier,
...(poNumber !== undefined ? { poNumber } : draft?.poNumber !== undefined ? { poNumber: draft.poNumber } : {}),
});
if (draft) invoice.id = draft.id;
accounting.invoices.save(invoice);
generated.push(invoice.id);
}
res.json({ generated, skipped });
});
router.get('/invoices', (req, res) => {
const { customerId, period, status } = req.query;
res.json({
invoices: accounting.invoices.list({
...(typeof customerId === 'string' ? { customerId } : {}),
...(typeof period === 'string' ? { period } : {}),
...(typeof status === 'string' ? { status: status as Invoice['status'] } : {}),
}),
});
});
router.get('/invoices/:id', (req, res) => {
const invoice = accounting.invoices.get(req.params.id);
if (!invoice) {
res.status(404).json({ error: 'invoice not found' });
return;
}
if (req.query.format === 'html') {
const name = customers.list().find((c) => c.id === invoice.customerId)?.name ?? invoice.customerId;
res.type('html').send(renderInvoiceHtml(invoice, name));
return;
}
res.json(invoice);
});
router.post('/invoices/:id/issue', (req, res) => {
const invoice = accounting.invoices.get(req.params.id);
if (!invoice) {
res.status(404).json({ error: 'invoice not found' });
return;
}
if (invoice.status !== 'draft') {
res.status(409).json({ error: `invoice is ${invoice.status}, not draft` });
return;
}
const now = Date.now();
const issued: Invoice = {
...invoice,
status: 'issued',
issuedAtMs: now,
...(invoice.billingType === 'purchase_order'
? { dueAtMs: now + 30 * 24 * 60 * 60 * 1000 }
: {}),
};
// Prepaid drawdown: a balance that fully covers the billable amount is
// deducted and the invoice goes straight to paid. Partial coverage stays
// untouched (no partial payments).
const customer = customers.list().find((c) => c.id === invoice.customerId);
const balance = customer?.balanceCents ?? 0;
if (customer && invoice.billableCents > 0 && balance >= invoice.billableCents) {
customers.save({ ...customer, balanceCents: balance - invoice.billableCents });
const paid: Invoice = { ...issued, status: 'paid', paidAtMs: now };
accounting.invoices.save(paid);
res.json(paid);
return;
}
accounting.invoices.save(issued);
res.json(issued);
});
router.post('/invoices/:id/paid', (req, res) => {
const invoice = accounting.invoices.get(req.params.id);
if (!invoice) {
res.status(404).json({ error: 'invoice not found' });
return;
}
if (invoice.status !== 'issued') {
res.status(409).json({ error: `invoice is ${invoice.status}, not issued` });
return;
}
const paid: Invoice = { ...invoice, status: 'paid', paidAtMs: Date.now() };
accounting.invoices.save(paid);
res.json(paid);
});
/* ---------------- accounting: reports ---------------- */
router.get('/reports/billing', (req, res) => {
const all = customers.list().flatMap((c) => accounting.usage.listFor(c.id));
const rows = billingRows({
entries: all,
customers: customers.list(),
tiers: store.getTiers(),
...(parseDate(req.query.from) ? { from: parseDate(req.query.from)! } : {}),
...(parseDate(req.query.to) ? { to: parseDate(req.query.to)! } : {}),
...(typeof req.query.customerId === 'string' ? { customerId: req.query.customerId } : {}),
...(typeof req.query.billingType === 'string'
? { billingType: req.query.billingType as 'stripe' | 'purchase_order' }
: {}),
});
if (req.query.format === 'csv') {
const csv = toCsv(rows, [
{ key: 'customerId', label: 'Customer Id' },
{ key: 'name', label: 'Name' },
{ key: 'billingType', label: 'Billing Type' },
{ key: 'calls', label: 'Calls' },
{ key: 'totalCents', label: 'Total Cents' },
{ key: 'creditCents', label: 'Credit Cents' },
{ key: 'billableCents', label: 'Billable Cents' },
]);
res
.type('text/csv')
.set('content-disposition', 'attachment; filename="billing-report.csv"')
.send(csv);
return;
}
res.json({ rows });
});
router.get('/reports/usage-trend', (req, res) => {
const from = parseDate(req.query.from);
const to = parseDate(req.query.to);
const customerId = typeof req.query.customerId === 'string' ? req.query.customerId : undefined;
const entries = customers
.list()
.filter((c) => (customerId ? c.id === customerId : true))
.flatMap((c) => accounting.usage.listFor(c.id))
.filter((e) => (!from || e.timestamp >= from) && (!to || e.timestamp < to));
const bucket = req.query.bucket === 'week' ? 'week' : 'day';
res.json({ points: usageTrend(entries, bucket) });
});
/* ---------------- admin user management ---------------- */
const USERNAME_RE = /^[a-zA-Z0-9_.-]+$/;
router.get('/users', (req, res) => {
res.json({
users: accounting.users.list().map((u) => ({
id: u.id,
username: u.username,
active: u.active,
createdMs: u.createdMs,
})),
});
});
router.post('/users', (req, res) => {
const { username, password } = req.body ?? {};
if (typeof username !== 'string' || !USERNAME_RE.test(username)) {
res.status(400).json({ error: 'username must match [a-zA-Z0-9_.-]+' });
return;
}
if (typeof password !== 'string' || password.length < 8) {
res.status(400).json({ error: 'password must be at least 8 characters' });
return;
}
if (accounting.users.findByUsername(username)) {
res.status(409).json({ error: 'username already exists' });
return;
}
const user = makeAdminUser(username, password);
accounting.users.save(user);
res.status(201).json({
id: user.id,
username: user.username,
active: user.active,
createdMs: user.createdMs,
});
});
const setActive = (active: boolean): RequestHandler => (req, res) => {
const user = accounting.users.findByUsername(req.params.username);
if (!user) {
res.status(404).json({ error: 'admin user not found' });
return;
}
if (!active) {
const activeCount = accounting.users.list().filter((u) => u.active).length;
if (user.active && activeCount <= 1) {
res.status(400).json({ error: 'cannot deactivate the last active admin user' });
return;
}
}
accounting.users.save({ ...user, active });
res.json({ ok: true });
};
router.post('/users/:username/deactivate', setActive(false));
router.post('/users/:username/activate', setActive(true));
/* ---------------- system: Zapier integration status ---------------- */
router.get('/zapier/status', (req, res) => {
const dir = path.join(PROJECT_ROOT, 'zapier-app');
const readDir = (sub: string): string[] => {
try {
return fs
.readdirSync(path.join(dir, sub))
.filter((f) => f.endsWith('.js'))
.map((f) => f.replace(/\.js$/, ''));
} catch {
return [];
}
};
let version: string | undefined;
try {
version = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version;
} catch {
version = undefined;
}
res.json({
appDirPresent: fs.existsSync(dir),
version,
triggers: readDir('triggers'),
creates: readDir('creates'),
});
});
return router;
}