master-zapier-plan-draft/packages/zappier/src/auth.ts
George Lambert b4150c8250 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.
2026-09-09 02:37:36 -04:00

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();
};
}