Skip to content

Put the CAPTCHA challenge where the visitor is looking - #16

Merged
boubou666 merged 2 commits into
mainfrom
worktree-turnstile-in-the-gate
Aug 7, 2026
Merged

boubou666 merged 2 commits into
mainfrom
worktree-turnstile-in-the-gate

Conversation

@boubou666

@boubou666 boubou666 commented Aug 7, 2026 •

Copy link
Copy Markdown
Owner

createTurnstileCaptcha appended its container to document.body, so an interactive challenge rendered wherever the layout dropped it — below a gate that starts 96px down a 340px column. Attached, as it has to be, but off-screen: sign-in appears to hang rather than to be waiting on someone. Cloudflare's managed mode challenges unfamiliar browsers and one was seen live on 2026-08-05, so this is a path real visitors take.

What changed

  • The widget looks up [data-captcha-mount], falling back to document.body.
  • RequireAccount renders that mount in every state that can ask for a token, "Loading…" included. That is the important one: a first visit's anonymous sign-in fires from the gate's effect before any form exists, and an unfamiliar browser is exactly what managed mode challenges — so the old code was worst precisely where a visitor had no other UI to explain itself.
  • appearance: 'interaction-only', so the widget occupies nothing until Cloudflare actually wants an answer. The default renders a box on every silent token, including that first-visit one.
  • .captcha-mount:empty { display: none }, so a gate that is never challenged carries no gap.

The comment claiming the container was "sized to nothing" is removed rather than corrected — nothing sized it.

Verification

Both new unit tests were run against the old code and fail there (container to the body; missing appearance), while the two fallback tests pass under both, which is correct — they describe behaviour this does not change. Full local suite 441/441, build clean.

Two things are not verified, and the PR should not be read as claiming they are:

  1. The browser assertion in test/browser/auth.test.js has never run. It needs local Supabase, which needs Docker, which is unavailable on the machine this was written on — CI is the first thing to execute it.
  2. interaction-only has never met real Cloudflare. Nothing in this repo has; the tests drive a fake widget.

The browser assertion measures where a widget-sized probe actually lands rather than reading the empty element — a display: none rect of zeros reports top: 0 and would pass any position asked of it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H6pCyUrdjterihUB8XQopQ

Summary by CodeRabbit

  • New Features

    • CAPTCHA challenges now appear within the relevant account or authentication interface when a designated placement is available.
    • CAPTCHA widgets use an interaction-only appearance and remain responsive within narrow viewports.
    • Widgets fall back to the page body when no designated placement is available.
  • Bug Fixes

    • Improved CAPTCHA placement and visibility across loading, account-gate, and adoption-failure states.
    • Empty CAPTCHA areas no longer add unnecessary spacing to the interface.

`createTurnstileCaptcha` appended its container to `document.body`, so an
interactive challenge rendered wherever the layout dropped it — below a
gate that starts 96px down a 340px column. Attached, as it must be, but
off-screen: sign-in appears to hang rather than to be waiting on someone.
Cloudflare's managed mode challenges unfamiliar browsers, and one was
seen live on 2026-08-05, so this is a path real visitors take.

The widget now looks for `[data-captcha-mount]` and falls back to the
body. The gate renders that mount in every state that can ask for a
token — including "Loading…", which is where a first visit's anonymous
sign-in happens, before any form exists and with no other UI to explain
itself.

It also renders with `appearance: 'interaction-only'`, so it occupies
nothing until Cloudflare actually wants an answer; the default would show
a box on every silent token, including that first-visit one. The mount is
`display: none` while `:empty`, so a gate that is never challenged
carries no gap.

The comment claiming the container was "sized to nothing" was never true
— nothing sized it. It is gone rather than corrected.

Both new unit tests were checked against the old code and fail there. The
browser assertion measures where a widget-sized probe actually lands
rather than reading the empty element, whose display:none rect of zeros
would have passed any position asked of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6pCyUrdjterihUB8XQopQ
@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aa9e43b1-3ed0-4fe5-a74b-0439bc2e5aca

📥 Commits

Reviewing files that changed from the base of the PR and between d8de919 and f464fb1.

📒 Files selected for processing (2)
  • src/styles/shell.css
  • test/browser/auth.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/browser/auth.test.js

📝 Walkthrough

Walkthrough

CAPTCHA widgets now mount in configured account-gate elements when available, with a body fallback. Account-gate states render shared mount points, and styles keep populated widgets centered and visible. Node and browser tests cover placement, fallback, appearance, and viewport fit.

Changes

CAPTCHA mount integration

Layer / File(s) Summary
Turnstile mount selection and widget appearance
src/platform/turnstile.js, test/node/turnstile.test.js
MOUNT_SELECTOR is exported. Each CAPTCHA request checks for a mount element, falls back to document.body, and uses interaction-only appearance. Tests cover each placement path.
Account-gate mount rendering and layout
src/shell/RequireAccount.jsx, src/styles/shell.css, test/browser/auth.test.js
Account-gate states render the CAPTCHA mount. Styles hide empty mounts, center populated widgets, and support horizontal scrolling. Browser tests verify placement and viewport visibility.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant RequireAccount
  participant createTurnstileCaptcha
  participant CaptchaMount
  RequireAccount->>createTurnstileCaptcha: request CAPTCHA widget
  createTurnstileCaptcha->>CaptchaMount: find [data-captcha-mount]
  createTurnstileCaptcha->>CaptchaMount: append interaction-only widget
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: placing the CAPTCHA challenge within the account gate where the visitor interacts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 worktree-turnstile-in-the-gate

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/styles/shell.css`:
- Around line 93-98: Remove the permanent margin declaration from
.captcha-mount, and apply the 14px 0 4px spacing only through a visibility state
or selector that is set when the CAPTCHA widget is actually visible. Preserve
the existing centering, flex, overflow, and empty-container hiding behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aabe20be-02b0-400f-85fd-65ebacb2f8dd

📥 Commits

Reviewing files that changed from the base of the PR and between 9a7cb95 and d8de919.

📒 Files selected for processing (5)
  • src/platform/turnstile.js
  • src/shell/RequireAccount.jsx
  • src/styles/shell.css
  • test/browser/auth.test.js
  • test/node/turnstile.test.js

Comment thread src/styles/shell.css
CodeRabbit caught this and it is right. `captcha()` attaches its
container before Cloudflare has decided whether to show a challenge, so
`.captcha-mount` is non-empty for the length of every token fetch — and
with `appearance: 'interaction-only'` almost every one of those is
silent. A margin on the wrapper is therefore reserved for an invisible
widget and taken away again: a gap that flickers into the gate on each
sign-in, including the one behind "Loading…" on a first visit.

The margin is gone. Whatever room a visible challenge needs comes from
the widget Cloudflare renders, which is the only thing that knows it is
visible. `:empty` still earns its keep — with no site key there is no
container and no call at all, which is every test and every local dev
server.

The comment that went with the old rule claimed the injected iframe was
what stopped `:empty` matching, and brought the margin with it. That was
wrong on both counts: our own container is appended first, and it is
appended whether or not anything visible follows. Rewritten rather than
trimmed, since the reasoning was the part that was broken.

The browser test now pins the case: a hidden child stands in for a silent
token, and the gate's height must not change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6pCyUrdjterihUB8XQopQ
@boubou666
boubou666 merged commit 06a851f into main Aug 7, 2026
2 checks passed
@boubou666
boubou666 deleted the worktree-turnstile-in-the-gate branch August 7, 2026 10:51
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.

2 participants