Repository navigation
Protect the website forms with Cloudflare Turnstile - #117
Merged
Merged
Conversation
The contact, support, partner-program, Slack invite and landing-page forms all go through `FormReactComponent`, which used to run invisible reCAPTCHA v3 on submit. The server only ever checked `success`, never the v3 score, so any bot driving a real browser passed. This swaps the widget for Cloudflare Turnstile, the same supplier the dashboard signup moved to. - `~/lib/loadTurnstile` loads api.js lazily with `render=explicit`; falls back to Cloudflare's always-pass test sitekey in development. - `useTurnstile` renders an `interaction-only` widget with `execution: 'execute'`, so nothing is visible unless Cloudflare wants a click, and a fresh single-use token is solved at submit time like before. - `~/lib/turnstile` verifies against siteverify and checks the `action` label matches the form. No secret → check disabled for that environment; Cloudflare unreachable or `internal-error` → fail open and log, mirroring `VerifiesCaptcha` in the API. - Each form carries its own `turnstileAction` (contact, support, ...) so the Turnstile analytics tell them apart. - `TURNSTILE_SITE_KEY` is a build-time public var, `TURNSTILE_SECRET_KEY` a runtime secret via `cubo config:set`. The sitekey slots in cubo.json5 are left empty until the widget is created in Cloudflare. The newsletter and docs-feedback forms still use reCAPTCHA; they move in the next commit, together with the reCAPTCHA removal. Claude-Session: https://claude.ai/code/session_01QhGrJ2y278ixVwiwZdSaEi
The two web-component forms get a `solveTurnstile(container, action)` helper that renders an interaction-only widget on submit and resolves with the token, the same shape the old `loadRecaptcha().execute()` had. With every form on Turnstile, reCAPTCHA goes away: the two `~/lib` modules, the `react-recaptcha-hook` dependency, and the `RECAPTCHA_KEY` / `RECAPTCHA_SECRET_KEY` wiring in astro.config, Dockerfile, cubo.json5 and .env.example. Left alone on purpose: the `pipedrive.createSalesLead` and `pipedrive.createPartnerProgramLead` actions still accept a `token` they never verify (no callers in this repo), and the GDPR / privacy documents still list Google reCAPTCHA as a sub-processor. Claude-Session: https://claude.ai/code/session_01QhGrJ2y278ixVwiwZdSaEi
Cloudflare's client-side rendering docs call `turnstile.execute` with the container while `reset`/`remove`/`getResponse` take the widget ID. Follow the documented shape instead of assuming the ID works for both. Claude-Session: https://claude.ai/code/session_01QhGrJ2y278ixVwiwZdSaEi
sistrall
force-pushed
the
cd16/ft/turnstile-on-forms
branch
from
September 7, 2026 09:13
355d557 to
0159925
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
After moving the dashboard signup to Cloudflare Turnstile (Basecamp card 10185982929), the sales/contact form on the website is next. While looking at it: every website form ran invisible reCAPTCHA v3, but the server only ever checked
successand ignored the v3 score, so any bot driving a real browser got through. The OpenAI spam classifier was doing all the real work.What
Every form on the site now runs Turnstile, and reCAPTCHA is gone.
FormReactComponent: contact, support, partner program, Slack invite,lp/migcs,lp/nuxt2eol,lp/modernize-cms): auseTurnstilehook renders aninteraction-onlywidget withexecution: 'execute', so nothing is visible unless Cloudflare wants a click, and a fresh single-use token is solved at submit time like before.solveTurnstile(container, action)helper does the same on submit.src/lib/turnstile.ts): verifies against siteverify and checks theactionlabel matches the form. No secret configured → check disabled for that environment. Cloudflare unreachable orinternal-error→ fail open and log to Rollbar, mirroringVerifiesCaptchain the API. Cloudflare's testing keys never echoaction, so that check is skipped when the response is flagged as a testing-key result.contact,support,partner-program,slack-invite,lp-*,newsletter,docs-feedback) so the Turnstile analytics tell the forms apart.TURNSTILE_SITE_KEYis a build-time public var (astro.config, Dockerfile,cubo.json5build args),TURNSTILE_SECRET_KEYa runtime secret viacubo config:set. Both optional. In development the code falls back to Cloudflare's always-pass test keys, so no local setup is needed.src/lib/recaptcha.ts,src/lib/loadRecaptcha.ts,react-recaptcha-hook, and theRECAPTCHA_KEY/RECAPTCHA_SECRET_KEYwiring.Before deploying
www.datocms.comand the staging hostnames (separate from the dashboard widget).TURNSTILE_SITE_KEYslots incubo.json5.cubo config:set TURNSTILE_SECRET_KEY=<value> --env <env>per environment. Until the secret is set, verification is skipped there, so staging can go first.Verified
astro checkpasses.Left alone on purpose
src/documents/gdpr.mdandprivacy.jsonstill list Google reCAPTCHA as a sub-processor; they should mention Cloudflare Turnstile instead.pipedrive.createSalesLeadandpipedrive.createPartnerProgramLeadaccept atokenthey never verify and have no callers in this repo.https://claude.ai/code/session_01QhGrJ2y278ixVwiwZdSaEi