Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions netlify/functions/xt26-register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ export const handler = async (event: NetlifyEvent): Promise<NetlifyResponse> =>
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()
Expand Down
16 changes: 16 additions & 0 deletions src/components/ContactUsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */}
<input
type='text'
tabIndex={-1}
autoComplete='off'
aria-hidden='true'
{...register('website')}
style={{ position: 'absolute', left: '-9999px', width: '1px', height: '1px', opacity: 0 }}
/>

{/* Row 1: Name fields */}
<div className='grid grid-cols-1 sm:grid-cols-2 gap-8'>
<input
Expand Down
Loading