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:
commit
b4150c8250
1364 changed files with 6814366 additions and 0 deletions
83
packages/zappier/src/auth.ts
Normal file
83
packages/zappier/src/auth.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
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();
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue