import { timingSafeEqual } from "node:crypto"; import type { FastifyInstance } from "fastify"; function safeEqual(a: string, b: string): boolean { const ba = Buffer.from(a || ""); const bb = Buffer.from(b || ""); if (ba.length !== bb.length) return false; return timingSafeEqual(ba, bb); } /** * Public, secret-gated donor-eligibility lookup for the FluentForms checkout. * The form's JS calls this on email blur to decide whether to unlock a donor * discount. Deliberately minimal: returns only { eligible, tier } — never * names or dollar amounts — so even with the (page-source-visible) secret it * can't leak donor financials. Rate-limited and CORS-restricted. */ export async function publicLookupRoutes(app: FastifyInstance): Promise { const cfg = app.ctx.config; const origin = cfg.PUBLIC_LOOKUP_ORIGIN; const cors = (reply: any) => { reply.header("Access-Control-Allow-Origin", origin); reply.header("Vary", "Origin"); reply.header("Access-Control-Allow-Methods", "GET, OPTIONS"); }; // Preflight (in case the form sends one). app.options("/api/public/donor-eligibility", async (_req, reply) => { cors(reply); return reply.code(204).send(); }); app.get( "/api/public/donor-eligibility", { config: { rateLimit: { max: 30, timeWindow: "1 minute" } } }, async (req, reply) => { cors(reply); // Disabled unless configured. if (!cfg.PUBLIC_LOOKUP_SECRET || !app.ctx.donors.enabled) { return reply.code(404).send({ error: "not_available" }); } const { key, email } = (req.query ?? {}) as { key?: string; email?: string }; if (!key || !safeEqual(key, cfg.PUBLIC_LOOKUP_SECRET)) { return reply.code(401).send({ error: "unauthorized" }); } const addr = String(email ?? "").trim(); if (!addr) return { eligible: false, tier: null }; try { const d = await app.ctx.donors.lookup(addr); const tier = d.found ? (d.isMember ? "member" : "donor") : null; return { eligible: d.found, tier }; } catch { // Fail closed — no discount rather than an error the form can't handle. return { eligible: false, tier: null }; } }, ); }