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.
83 lines
2.4 KiB
TypeScript
83 lines
2.4 KiB
TypeScript
import { RequestHandler } from 'express';
|
|
|
|
export type BillingType = 'stripe' | 'purchase_order';
|
|
|
|
export interface Customer {
|
|
id: string;
|
|
name: string;
|
|
tierId: string;
|
|
apiKey: string;
|
|
stripeCustomerId?: string;
|
|
multiplierOverride?: number;
|
|
/** How this customer is billed. Absent means 'stripe' (the default). */
|
|
billingType?: BillingType;
|
|
email?: string;
|
|
/** scrypt hash for portal login. Absent = no portal password set yet. */
|
|
passwordHash?: string;
|
|
/** Base32 TOTP secret. Present once 2FA setup begins. */
|
|
totpSecret?: string;
|
|
/** True once the customer has confirmed a TOTP code. */
|
|
totpEnabled?: boolean;
|
|
/** Prepaid balance in cents, drawn down when invoices are issued. */
|
|
balanceCents?: number;
|
|
/** Email a copy of each issued invoice to the customer's address. */
|
|
emailInvoicing?: boolean;
|
|
}
|
|
|
|
export interface CustomerRepo {
|
|
findByApiKey(apiKey: string): Customer | undefined;
|
|
findByEmail(email: string): Customer | undefined;
|
|
list(): Customer[];
|
|
save(customer: Customer): void;
|
|
}
|
|
|
|
export class InMemoryCustomerRepo implements CustomerRepo {
|
|
private customers: Customer[];
|
|
|
|
constructor(customers: Customer[] = []) {
|
|
// Defensive copy: save() replaces elements, and callers often pass shared
|
|
// seed arrays (DEFAULT_CUSTOMERS) that must not be mutated across tests.
|
|
this.customers = customers.map((c) => ({ ...c }));
|
|
}
|
|
|
|
findByApiKey(apiKey: string): Customer | undefined {
|
|
return this.customers.find((c) => c.apiKey === apiKey);
|
|
}
|
|
|
|
findByEmail(email: string): Customer | undefined {
|
|
const needle = email.toLowerCase();
|
|
return this.customers.find((c) => c.email?.toLowerCase() === needle);
|
|
}
|
|
|
|
list(): Customer[] {
|
|
return [...this.customers];
|
|
}
|
|
|
|
save(customer: Customer): void {
|
|
const i = this.customers.findIndex((c) => c.id === customer.id);
|
|
if (i >= 0) this.customers[i] = customer;
|
|
else this.customers.push(customer);
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
namespace Express {
|
|
interface Request {
|
|
customer?: Customer;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function apiKeyAuth(repo: CustomerRepo): RequestHandler {
|
|
return (req, res, next) => {
|
|
const key = req.header('x-api-key');
|
|
const customer = key ? repo.findByApiKey(key) : undefined;
|
|
if (!customer) {
|
|
res.status(401).json({ error: 'invalid or missing API key' });
|
|
return;
|
|
}
|
|
req.customer = customer;
|
|
next();
|
|
};
|
|
}
|