Skip to content

improvement(contact): add Turnstile CAPTCHA, honeypot, and robustness fixes#4248

Merged
waleedlatif1 merged 6 commits intostagingfrom
improvement/contact
Apr 22, 2026
Merged

improvement(contact): add Turnstile CAPTCHA, honeypot, and robustness fixes#4248
waleedlatif1 merged 6 commits intostagingfrom
improvement/contact

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Add Cloudflare Turnstile CAPTCHA with graceful degradation — when the widget fails to load (ad blockers, iOS privacy, corporate DNS), submissions fall through to a tighter rate-limit bucket instead of hard-blocking legitimate users
  • Add honeypot field to silently discard bot submissions
  • Add separate `CAPTCHA_UNAVAILABLE_RATE_LIMIT` bucket (3/min) for the no-captcha path so spam via ad-blocker bypass remains expensive
  • Pass `expectedHostname` to `verifyTurnstileToken` to close cross-site token reuse gap
  • Move `turnstile.ts` to `lib/core/security/` alongside csp/encryption/input-validation
  • Wire `onExpire`/`onError`/`onUnsupported` callbacks so token expiry during slow form-filling falls back gracefully
  • Add 30s timeout to `getResponsePromise` to prevent indefinite hang on network blips
  • Add `size: 'invisible'` to Turnstile options (required for execute mode)
  • Switch all CSS to `--landing-*` variables throughout contact form
  • Move error display inline next to label with truncation in `LandingField`
  • Add `labelClassName` prop to `LandingField` for context-specific overrides
  • Simplify contact page to single-column `max-w-[640px]` layout

Type of Change

  • Improvement (enhancement to existing feature)

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

… fixes

- Add Cloudflare Turnstile with graceful degradation: when the widget
  fails to load (ad blockers, iOS privacy, corporate DNS), submissions
  fall through to a tighter rate-limit bucket rather than hard-blocking
- Add honeypot field to filter automated submissions without user impact
- Add separate CAPTCHA_UNAVAILABLE_RATE_LIMIT bucket (3/min) for the
  no-captcha path so spam via ad-blocker bypass remains expensive
- Pass expectedHostname to verifyTurnstileToken to close cross-site
  token reuse gap
- Add SITE_HOSTNAME as module-level constant (avoid URL parsing per req)
- Wire onExpire/onError/onUnsupported callbacks so token expiry during
  slow form-filling falls back gracefully instead of showing a captcha error
- Add getResponsePromise(30_000) timeout to prevent indefinite hang on
  network blips
- Add size: 'invisible' to Turnstile options (required for execute mode)
- Move turnstile.ts to lib/core/security/ alongside csp/encryption/input-validation
- Switch all CSS to --landing-* variables throughout contact form
- Move error display inline next to label with truncation in LandingField
- Add labelClassName prop to LandingField for context-specific overrides
- Simplify contact page to single-column max-w-[640px] layout
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Apr 22, 2026 1:16am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 22, 2026

PR Summary

Medium Risk
Adds new anti-abuse and verification logic to the public POST /api/contact endpoint plus client-side Turnstile execution, which could impact legitimate submissions if misconfigured or if Turnstile verification/rate-limit fallback behavior is incorrect.

Overview
Hardens the contact flow by adding a hidden honeypot field and Cloudflare Turnstile CAPTCHA support with graceful degradation: the client executes an invisible widget when configured and sends captchaToken/captchaUnavailable, and the server verifies tokens (including expectedHostname) or falls back to a stricter no-captcha rate-limit bucket.

Updates the landing contact UI by restyling inputs to --landing-* tokens, adding a Privacy Policy notice, improving error rendering in LandingField (inline/truncated with new labelClassName), and simplifying the /contact page to a single-column layout.

Reviewed by Cursor Bugbot for commit 1320e5d. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR hardens the contact form with Cloudflare Turnstile CAPTCHA, a honeypot field, a separate tighter rate-limit bucket for the no-CAPTCHA path, hostname validation on token verification, and graceful client-side degradation. The visual redesign migrates all styling to --landing-* CSS variables and simplifies the page to a single-column layout.

Confidence Score: 5/5

Safe to merge — all previously raised P0/P1 concerns are resolved; only a minor style suggestion remains.

Transport-error fallback (previously flagged P1) is correctly handled, honeypot positioning was fixed, and the CSS variable concern was confirmed intentional. The one remaining comment is a P2 style nit (as unknown as double cast for CONTACT_TOPIC_OPTIONS).

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/lib/core/security/turnstile.ts New Turnstile verification module — well-structured with proper timeout/abort, transport-error classification, and hostname validation. No issues found.
apps/sim/app/api/contact/route.ts CAPTCHA verification and honeypot integrated correctly; transport-error fallback applies the tighter rate-limit bucket as expected by the PR design. No issues found.
apps/sim/app/(landing)/components/contact/contact-form.tsx Turnstile widget integrated with graceful degradation; minor: as unknown as ComboboxOption[] double cast should use spread instead.
apps/sim/app/(landing)/components/forms/landing-field.tsx Inline error display with labelClassName prop; aria-describedby relationship preserved. No issues.
apps/sim/app/(landing)/contact/page.tsx Simplified to single-column layout; removes direct-contact sidebar. Straightforward layout change with no issues.

Sequence Diagram

sequenceDiagram
    participant User
    participant Form as ContactForm (client)
    participant TW as Turnstile Widget
    participant API as /api/contact
    participant CF as Cloudflare siteverify

    User->>Form: Submit
    Form->>Form: Validate fields (Zod)
    alt Turnstile site key configured
        alt widgetReady
            Form->>TW: reset() + execute()
            TW-->>Form: captchaToken (or timeout/error)
        else widget unavailable/expired
            Form->>Form: captchaUnavailable=true
        end
    end
    Form->>API: POST {fields, captchaToken?, captchaUnavailable?, website}
    API->>API: Check honeypot (website field)
    alt captchaUnavailable
        API->>API: CAPTCHA_UNAVAILABLE rate limit (3/min)
    else Turnstile configured and token present
        API->>CF: POST siteverify
        CF-->>API: {success, hostname, error-codes}
        alt transport error
            API->>API: CAPTCHA_UNAVAILABLE rate limit (3/min)
        else verification failed
            API-->>Form: 400 CAPTCHA failed
        end
    end
    API->>API: Validate body (Zod)
    API->>API: Send emails
    API-->>Form: 201 success
    Form->>User: Show success state
Loading

Reviews (3): Last reviewed commit: "fix(contact): disable submit during CAPT..." | Re-trigger Greptile

Comment thread apps/sim/app/api/contact/route.ts
Comment thread apps/sim/app/(landing)/components/contact/contact-form.tsx
Comment thread apps/sim/app/(landing)/components/contact/contact-form.tsx Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

Good catch on the transport error path — fixed in b734541. When verifyTurnstileToken returns transportError: true (Cloudflare unreachable, 5s timeout, non-2xx), we now fall through to the existing CAPTCHA_UNAVAILABLE_RATE_LIMIT bucket (3/min) rather than returning 400. A Cloudflare outage no longer hard-blocks users who completed the challenge.

P2a (--text-error variable): Intentional. No --landing-text-error token is defined in globals.css. The submit error paragraph is inside the .dark wrapper on the page, so --text-error resolves to its dark-mode value correctly.

P2b (missing relative on form): Not needed. The honeypot is hidden via five independent mechanisms (aria-hidden, pointer-events-none, opacity-0, h-px w-px, overflow-hidden) — its positioned ancestor is irrelevant to functionality. The left-[-9999px] off-screen placement works regardless of which ancestor it's relative to.

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/(landing)/components/contact/contact-form.tsx
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1320e5d. Configure here.

Comment thread apps/sim/app/api/contact/route.ts
@waleedlatif1 waleedlatif1 merged commit 34cfc26 into staging Apr 22, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the improvement/contact branch April 22, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant