Skip to content

Server-side CAPTCHA: Turnstile on free signup (+ web 4.4.0) - #910

Merged
feruzm merged 3 commits into
developfrom
feature/turnstile-signup
Jun 4, 2026
Merged

Server-side CAPTCHA: Turnstile on free signup (+ web 4.4.0)#910
feruzm merged 3 commits into
developfrom
feature/turnstile-signup

Conversation

@feruzm

@feruzmferuzm commented Jun 4, 2026

Copy link
Copy Markdown
Member

What

Replaces the client-only reCAPTCHA on the free signup page with Cloudflare Turnstile (Managed mode) and actually sends the token to the backend for server-side verification. The old reCAPTCHA token was never sent/verified, so the check was trivially bypassed.

Changes

  • features/shared/turnstile.tsx — small dependency-free Turnstile component (loads the CF script once, explicit render, resets the token on expire/error).
  • /signup/free — renders <Turnstile> instead of <ReCAPTCHA>; the submit button stays disabled until a token is obtained; the token is passed to signUp.
  • @ecency/sdksignUp() — optional captchaToken param → captcha_token in the POST body. Backward compatible (existing 3-arg callers unchanged).
  • Public sitekey via NEXT_PUBLIC_TURNSTILE_SITEKEY (site default baked in; it's public by design).
  • Bump @ecency/web to 4.4.0.

Rollout

Inert until the backend enables verification (CAPTCHA_MODE=softhard). The verification secret stays server-side only (onboard env); only the public sitekey is in the client. reCAPTCHA on the login page is intentionally left untouched (out of scope). Mobile widget + per-outbound-link "untrusted" styling are separate follow-ups.

Validation

Typecheck clean for the changed files; existing tests unaffected (no spec referenced the reCAPTCHA flow).

Summary by CodeRabbit

  • New Features
    • Upgraded free signup verification to use Cloudflare Turnstile for enhanced security.
    • Added error handling and user guidance when verification service fails to load.

Free signup now uses Cloudflare Turnstile (Managed mode) and actually SENDS the
token to the backend for server-side verification. The previous reCAPTCHA was
checked client-side only and the token was discarded, so it was trivially bypassed.
- New dependency-free <Turnstile> component (loads the CF script, explicit render,
expire/error reset).
- SDK signUp() accepts an optional captchaToken and includes captcha_token in the
request body (backward compatible; existing 3-arg callers unaffected).
- Public sitekey via NEXT_PUBLIC_TURNSTILE_SITEKEY (with the site default).
- Bump @ecency/web to 4.4.0.
Backend verification lands separately and is off by default until enabled; the
secret stays server-side only. Mobile + per-link untrusted styling are follow-ups.
@coderabbitai

coderabbitaiBot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 03ad86bb-1ff0-44a8-8f88-8c4e50b253fa

📥 Commits

Reviewing files that changed from the base of the PR and between cdc5676 and c7bc0f0.

⛔ Files ignored due to path filters (7)
  • packages/sdk/dist/browser/index.d.ts is excluded by !**/dist/**
  • packages/sdk/dist/browser/index.js is excluded by !**/dist/**
  • packages/sdk/dist/browser/index.js.map is excluded by !**/dist/**, !**/*.map
  • packages/sdk/dist/node/index.cjs is excluded by !**/dist/**
  • packages/sdk/dist/node/index.cjs.map is excluded by !**/dist/**, !**/*.map
  • packages/sdk/dist/node/index.mjs is excluded by !**/dist/**
  • packages/sdk/dist/node/index.mjs.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (9)
  • apps/web/package.json
  • apps/web/src/app/signup/free/_page.tsx
  • apps/web/src/features/i18n/locales/en-US.json
  • apps/web/src/features/shared/turnstile.tsx
  • packages/sdk/CHANGELOG.md
  • packages/sdk/package.json
  • packages/sdk/src/modules/private-api/requests.ts
  • packages/wallets/CHANGELOG.md
  • packages/wallets/package.json

📝 Walkthrough

Walkthrough

This PR migrates the free signup form from Google reCAPTCHA to Cloudflare Turnstile. It introduces a new reusable Turnstile React component with lazy script loading and error handling, extends the SDK signUp endpoint to accept a captcha token, integrates Turnstile into the signup form with proper state management and retry behavior, and updates localization strings and package versions across three packages.

Changes

Turnstile Verification Migration

Layer / File(s)Summary
Turnstile component and script loading
apps/web/src/features/shared/turnstile.tsx
New client-side React component integrating Cloudflare Turnstile in explicit render mode with cached script loading, widget rendering, token verification, programmatic reset, and i18n error messaging for script-load failures.
SDK signUp captchaToken support
packages/sdk/src/modules/private-api/requests.ts
Updated signUp function to accept optional captchaToken parameter and include it in the POST /private-api/account-create request payload as captcha_token.
Free signup form Turnstile integration
apps/web/src/app/signup/free/_page.tsx
Replaced reCAPTCHA with Turnstile component, added TURNSTILE_SITEKEY constant, managed captchaToken state, updated form submission to validate token presence, pass token to SDK, and programmatically reset widget on signup errors.
Localization and release metadata
apps/web/src/features/i18n/locales/en-US.json, apps/web/package.json, packages/sdk/CHANGELOG.md, packages/sdk/package.json, packages/wallets/CHANGELOG.md, packages/wallets/package.json
Added captcha-load-failed translation; bumped web to 4.4.0, SDK to 2.3.9, and wallets to 5.0.9 with changelog entries documenting Turnstile integration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A turnstile spins where reCAPTCHA stood,
Script-loaded once, with error handling so good,
Token flows onward through form and through SDK,
Versions climb upward—the migration's complete, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: replacing reCAPTCHA with Cloudflare Turnstile on the free signup page and bumping the web version.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/turnstile-signup

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/sdk/src/modules/private-api/requests.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

YAMLException: Cannot read config file: /packages/sdk/eslint.config.mjs
Error: end of the stream or a document separator is expected (5:12)

2 |
3 | export default tseslint.config(
4 | {
5 | ignores: ["dist", "node_modules", "tsup ...
----------------^
6 | },
7 | ...tseslint.configs.recommended,
at generateError (/node_modules/.pnpm/js-yaml@4.1.1/node_modules/js-yaml/lib/loader.js:199:10)
at throwError (/node_modules/.pnpm/js-yaml@4.1.1/node_modules/js-yaml/lib/loader.js:203:9)
at readDocument (/node_modules/.pnpm/js-yaml@4.1.1/node_modules/js-yaml/lib/loader.js:1651:5)
at loadDocuments (/node_modules/.pnpm/js-yaml@4.1.1/node_modules/js-yaml/lib/loader.js:1694:5)
at Object.load (/node_modules/.pnpm/js-yaml@4.1.1/node_modules/js-yaml/lib/loader.js:1720:19)
at loadLegacyConfigFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2565:21)
at loadConfigFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2680:20)
at ConfigArrayFactory._loadConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:42)
at ConfigArrayFactory.loadFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
at createCLIConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:3cb3769cc0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

const response = await signUp(username, email, referral);
const response = await signUp(username, email, referral, captchaToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Rebuild SDK dist before passing the CAPTCHA token

In the web app, @ecency/sdk resolves through the package exports to the checked-in packages/sdk/dist/* artifacts, but this commit only changed packages/sdk/src; I checked packages/sdk/dist/browser/index.js and packages/sdk/dist/node/index.mjs, and their signUp implementations still accept three arguments and serialize only { username, email, referral }. This fourth argument is therefore dropped in the production bundle, so once the backend requires Turnstile verification the free signup request will still arrive without captcha_token and be rejected.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-appsBot commented Jun 4, 2026

Copy link
Copy Markdown

Greptile Summary

Replaces client-only reCAPTCHA on the free signup page with Cloudflare Turnstile (Managed mode) and wires the token to the backend for actual server-side verification. The old reCAPTCHA token was never sent to the backend, so this closes a trivially bypassable signup gate.

  • turnstile.tsx — new self-contained widget; loads the CF script once via a module-level promise, tracks the widget ID for explicit cleanup, stores callbacks in refs to avoid stale closures, exposes an imperative reset() handle via forwardRef, and renders a user-facing error string when the script fails to load.
  • _page.tsx — swaps <ReCAPTCHA> for <Turnstile>; keeps the submit button disabled until a token arrives; calls setCaptchaToken(\"\") and turnstileRef.current?.reset() in both failure branches so single-use tokens can't leak into a retry.
  • sdk/requests.ts — adds an optional captchaToken param to signUp(); backward-compatible (undefined values are omitted from the JSON body by JSON.stringify).

Confidence Score: 5/5

Safe to merge; all previous review concerns have been addressed and no new functional defects are introduced.

The Turnstile component is cleanly implemented — script deduplication, proper cleanup on unmount, stable callbacks via refs, and imperative reset all work correctly. The signup page correctly gates submission on a live token and resets both the state and the widget on every failure branch. The SDK change is backward-compatible.

No files require special attention; the core changes in turnstile.tsx and _page.tsx are self-consistent.

Important Files Changed

FilenameOverview
apps/web/src/features/shared/turnstile.tsxNew dependency-free Cloudflare Turnstile widget; correctly uses refs for stable callbacks, useImperativeHandle for reset, cleanup on unmount, failedToLoad error state, and deduplicates the script load via a module-level promise.
apps/web/src/app/signup/free/_page.tsxReplaces ReCAPTCHA with Turnstile; properly resets the token and widget on both error branches; submit button gated on a non-empty captchaToken. Minor: registrationError is never cleared at the start of a retry, leaving a stale error message visible while the new request is in-flight.
packages/sdk/src/modules/private-api/requests.tsAdds optional captchaToken param to signUp(); emits captcha_token only when provided (undefined is omitted by JSON.stringify), preserving backward compatibility for existing 3-arg callers.
apps/web/src/features/i18n/locales/en-US.jsonAdds captcha-check-required and captcha-load-failed i18n strings required by the new Turnstile component and signup page.

Fix All in Claude Code

Reviews (3): Last reviewed commit: "chore: apply changeset versioning for PR..." | Re-trigger Greptile

Comment threadapps/web/src/app/signup/free/_page.tsx
Comment threadapps/web/src/features/shared/turnstile.tsx
Comment threadapps/web/src/features/shared/turnstile.tsx Outdated
…stness
- Turnstile tokens are single-use: clear the token and reset the widget after a
failed signup (error code or thrown error) so a retry gets a fresh challenge
instead of a confusing CAPTCHA failure. Turnstile now exposes a reset() handle.
- Hold onVerify/onExpire in refs so the widget renders once but always calls the
latest callbacks (no stale closures if a caller passes inline callbacks).
- Show a visible message when the Turnstile script fails to load (ad/script
blocker, CSP) instead of leaving the submit button silently disabled.
@feruzmferuzm added the patch Bug fixes and patches (1.0.0 → 1.0.1) label Jun 4, 2026
@feruzm
feruzm merged commit 6f88f37 into developJun 4, 2026
3 of 4 checks passed
@feruzm
feruzm deleted the feature/turnstile-signup branch June 4, 2026 21:50
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patchBug fixes and patches (1.0.0 → 1.0.1)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@feruzm