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

192
packages/zappier/src/app.ts Normal file
View file

@ -0,0 +1,192 @@
import path from 'path';
import { randomUUID } from 'crypto';
import express, { Express, NextFunction, Request, RequestHandler, Response } from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs';
import {
apiKeyAuth,
Customer,
CustomerRepo,
InMemoryCustomerRepo,
} from './auth';
import { meter } from './meter';
import { applyMonthlyCredit } from './billing/credit';
import {
ConfigTierCatalog,
InMemoryPricingStore,
PricingContext,
PricingStore,
} from './pricing';
import { InMemoryUsageRepo, UsageRepo } from './usage';
import { adminAuth, adminLoginRouter, adminRouter } from './admin';
import { AdminUserRepo, InMemoryAdminUserRepo, seedAdminUsersFromEnv } from './admin-users';
import { InMemorySessionRepo, SessionRepo } from './accounts';
import { InMemoryInvoiceRepo, InvoiceRepo } from './invoicing';
import { PROJECT_ROOT } from './paths';
import { PaymentClient, portalRouter } from './portal';
export interface StoredItem {
id: string;
customerId: string;
metadata: Record<string, unknown>;
attachments: { filename: string; size: number }[];
createdAt: string;
}
export const DEFAULT_CUSTOMERS: Customer[] = [
{ id: 'cust_1', name: 'Ada (free)', tierId: 'free', apiKey: 'key-ada' },
{ id: 'cust_2', name: 'Grace (pro)', tierId: 'pro', apiKey: 'key-grace' },
{ id: 'cust_3', name: 'Linus (business)', tierId: 'business', apiKey: 'key-linus' },
];
export interface AppDeps {
usage?: UsageRepo;
customers?: CustomerRepo;
pricingStore?: PricingStore;
invoices?: InvoiceRepo;
adminUsers?: AdminUserRepo;
sessions?: SessionRepo;
payments?: PaymentClient;
/** QR renderer for 2FA setup; defaults to the qrcode package. */
qr?: (uri: string) => Promise<string>;
}
const SPEC_PATH = path.join(PROJECT_ROOT, 'openapi.yaml');
const parseMetadata: RequestHandler = (req, res, next) => {
const raw = req.body?.metadata;
if (raw === undefined || raw === null || raw === '') {
res.locals.parsedMetadata = {};
return next();
}
if (typeof raw !== 'string') {
res.locals.parsedMetadata = raw;
return next();
}
try {
res.locals.parsedMetadata = JSON.parse(raw);
next();
} catch {
res.status(400).json({ error: 'invalid metadata JSON' });
}
};
export function buildApp(deps: AppDeps = {}): {
app: Express;
usage: UsageRepo;
customers: CustomerRepo;
pricing: PricingContext;
pricingStore: PricingStore;
invoices: InvoiceRepo;
items: StoredItem[];
} {
const customers = deps.customers ?? new InMemoryCustomerRepo(DEFAULT_CUSTOMERS);
const usage = deps.usage ?? new InMemoryUsageRepo();
const pricingStore = deps.pricingStore ?? new InMemoryPricingStore();
const invoices = deps.invoices ?? new InMemoryInvoiceRepo();
const adminUsers =
deps.adminUsers ?? InMemoryAdminUserRepo.seeded(seedAdminUsersFromEnv());
// Live pricing context: every quote reads the store, so admin edits apply immediately.
const pricing: PricingContext = {
get rateCard() {
return pricingStore.getRateCard();
},
get tiers() {
return new ConfigTierCatalog(pricingStore.getTiers());
},
};
const items: StoredItem[] = [];
const app = express();
app.use(express.json());
const spec = YAML.load(SPEC_PATH);
app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec));
app.use(
'/admin/api',
adminLoginRouter(adminUsers),
adminAuth(),
adminRouter(pricingStore, customers, { usage, invoices, users: adminUsers }),
);
app.use('/admin', express.static(path.join(PROJECT_ROOT, 'admin')));
const sessions = deps.sessions ?? new InMemorySessionRepo();
const qr =
deps.qr ??
(async (uri: string) => {
const qrcode = await import('qrcode');
return qrcode.toDataURL(uri, { margin: 1, width: 220 });
});
const payments: PaymentClient = deps.payments ?? {
// Safe default for tests/dev: credits immediately, no Stripe call.
reload: async (_customer, amountCents) => ({ mode: 'dev', creditedCents: amountCents }),
};
app.use(
'/portal/api',
portalRouter({
customers,
sessions,
usage,
invoices,
tiers: () => pricingStore.getTiers(),
rateCard: () => pricingStore.getRateCard(),
payments,
qr,
}),
);
app.use('/portal', express.static(path.join(PROJECT_ROOT, 'portal')));
app.use('/v1', apiKeyAuth(customers));
app.use(
OpenApiValidator.middleware({
apiSpec: SPEC_PATH,
validateRequests: true,
validateResponses: false,
fileUploader: { limits: { fileSize: 25 * 1024 * 1024 } },
}),
);
app.get('/v1/status', meter('status', usage, pricing), (req, res) => {
res.json({ status: 'ok', quote: res.locals.quote });
});
app.post('/v1/transform', meter('transform', usage, pricing), (req, res) => {
const text = String(req.body?.text ?? '');
res.json({ output: text.toUpperCase(), quote: res.locals.quote });
});
app.post('/v1/storage', parseMetadata, meter('storage', usage, pricing), (req, res) => {
const metadata = res.locals.parsedMetadata as Record<string, unknown>;
const files = (req.files as Express.Multer.File[]) ?? [];
const item: StoredItem = {
id: randomUUID(),
customerId: req.customer!.id,
metadata,
attachments: files.map((f) => ({ filename: f.originalname, size: f.size })),
createdAt: new Date().toISOString(),
};
items.unshift(item);
res.json({ id: item.id, quote: res.locals.quote });
});
app.get('/v1/storage', meter('storage-list', usage, pricing), (req, res) => {
res.json({ items: items.filter((i) => i.customerId === req.customer!.id) });
});
app.get('/v1/usage', (req, res) => {
const since = new Date();
since.setUTCDate(1);
since.setUTCHours(0, 0, 0, 0);
const summary = usage.summaryFor(req.customer!.id, since);
const tier = pricing.tiers.find(req.customer!.tierId);
res.json(tier ? applyMonthlyCredit(summary, tier) : summary);
});
app.use((err: Error & { status?: number }, req: Request, res: Response, next: NextFunction) => {
res.status(err.status ?? 500).json({ error: err.message });
});
return { app, usage, customers, pricing, pricingStore, invoices, items };
}