Add secret-gated public donor-eligibility lookup for checkout discount

GET /api/public/donor-eligibility?key=&email= returns only {eligible, tier}
(member/donor) — no names or dollar amounts — gated by PUBLIC_LOOKUP_SECRET,
rate-limited (30/min), and CORS-restricted to PUBLIC_LOOKUP_ORIGIN. Lets the
FluentForms checkout unlock a donor discount by email. Docs + ready-to-paste
form snippet in docs/fluentforms-donor-discount.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hank 2026-07-08 22:09:25 +00:00
parent 774d00ff5b
commit fb2fdcb6b8
4 changed files with 169 additions and 0 deletions

View file

@ -0,0 +1,60 @@
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<void> {
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 };
}
},
);
}