diff --git a/netlify/functions/xt26-register.ts b/netlify/functions/xt26-register.ts index 5a0355310..2ba059a10 100644 --- a/netlify/functions/xt26-register.ts +++ b/netlify/functions/xt26-register.ts @@ -43,6 +43,19 @@ export const handler = async (event: NetlifyEvent): Promise => return json(400, { error: 'invalid json' }) } + // Honeypot. The form renders a hidden, off-screen `website` input + // that humans never fill. A non-empty value means the submission is + // almost certainly a bot — discard it WITHOUT forwarding to Orbit, + // and return 200 so the bot can't tell its submission was dropped + // (a 4xx would let it detect the trap and tune around it). This is + // the primary gate: bots POST straight to this public function + // endpoint, so rejecting here stops the spam at the edge. Orbit's + // POST /api/people carries the same check as a backstop. + if ((body.website ?? '').trim() !== '') { + console.warn('xt26-register: honeypot tripped, dropping submission') + return json(200, { ok: true }) + } + const firstName = (body.firstName ?? '').trim() const lastName = (body.lastName ?? '').trim() const email = (body.email ?? '').trim().toLowerCase() diff --git a/src/components/ContactUsForm.tsx b/src/components/ContactUsForm.tsx index fb6aca2c2..a4af6fef6 100644 --- a/src/components/ContactUsForm.tsx +++ b/src/components/ContactUsForm.tsx @@ -126,6 +126,22 @@ export function ContactUsForm(props: ContactUsFormProps) { value='c2bf653a-2baa-466d-bbcc-390272663918' /> + {/* Honeypot. Off-screen (not display:none — sophisticated bots + skip hidden fields) and removed from the tab order, so a + human never sees or focuses it and it stays empty. A bot + that auto-fills every input trips it; the Netlify function + and Orbit both silently discard any submission where it's + non-empty. Plausible-but-unused field name so bots target + it. See juxt/orbit docs/notes/website-form-spam.md. */} + + {/* Row 1: Name fields */}