From 8ef6cbe464f0f14bae8c8cb15d129c20bb610d0c Mon Sep 17 00:00:00 2001 From: Dustin VanKrimpen Date: Fri, 3 Jul 2026 22:49:27 -0400 Subject: [PATCH 1/3] test(content): failing fixture for TPS managed-challenge detection Capture the /InternalCaptcha gap: an explicitly-rendered Cloudflare Turnstile (turnstile.render(...)) exposes no .cf-turnstile container and about:blank widget iframes, so detectChallenge() misses it and the sidebar shows verdict buttons over a live bot-gate. The reproducing fixture (API script present, no container, no response input -> true) is red; two regression locks ship green alongside it: - solved container-Turnstile WITH the api.js script -> false, proving the coming `!turnstile` guard defers the container case to the existing solved/unsolved logic (real solved Turnstile pages carry the script too). - Clym-consent-only (cf.clym-widget.net, no challenges.cloudflare.com script) -> false, locking the selector to the specific host so a bare "cf"/"cloudflare" substring can't false-positive. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/content/classify.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/content/classify.test.ts b/src/content/classify.test.ts index f85fc69..c45220b 100644 --- a/src/content/classify.test.ts +++ b/src/content/classify.test.ts @@ -60,4 +60,31 @@ describe('detectChallenge', () => { addEl(tag, attr, value); expect(detectChallenge()).toBe(true); }); + + // Explicitly-rendered Turnstile (turnstile.render(...)) — the TruePeopleSearch /InternalCaptcha + // managed-challenge shape: no .cf-turnstile container, widget iframes are about:blank, but the + // Turnstile API script is in the top document. That script is the only reliable signal. + it('explicitly-rendered Turnstile managed challenge (API script, no container) → true', () => { + addEl('script', 'src', 'https://challenges.cloudflare.com/turnstile/v0/api.js'); + expect(detectChallenge()).toBe(true); + }); + + // A real solved Turnstile page carries the api.js script too. The `!turnstile` guard must route + // the container case to the solved/unsolved logic above, so the new script branch does NOT + // re-detect a solved container-Turnstile just because the script is present. + it('solved container-Turnstile with the API script present → false (!turnstile guard)', () => { + document.body.innerHTML = + '
'; + addEl('script', 'src', 'https://challenges.cloudflare.com/turnstile/v0/api.js'); + expect(detectChallenge()).toBe(false); + }); + + // Trap: the /InternalCaptcha page also hosts a cf.clym-widget.net iframe (Clym consent widget, + // not Cloudflare). Keying off a bare "cf"/"cloudflare" substring would false-positive on it — the + // selector must match the specific challenges.cloudflare.com host only. + it('Clym consent widget only (cf.clym-widget.net, no Cloudflare challenge script) → false', () => { + addEl('iframe', 'src', 'https://cf.clym-widget.net/latest/api-bridge/?instance=us6.clym.io'); + addEl('script', 'src', 'https://cf.clym-widget.net/latest/loader.js'); + expect(detectChallenge()).toBe(false); + }); }); From 2c45b4e3da4a4f9dda92ab59b61a17335af1d1fa Mon Sep 17 00:00:00 2001 From: Dustin VanKrimpen Date: Fri, 3 Jul 2026 22:50:11 -0400 Subject: [PATCH 2/3] fix(content): detect explicitly-rendered Turnstile via API script host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detectChallenge() now returns true when the Turnstile API script (challenges.cloudflare.com/turnstile) is present in the top document, catching the TruePeopleSearch /InternalCaptcha managed challenge that renders Turnstile via turnstile.render(...) — no .cf-turnstile container, about:blank widget iframes — which every prior selector missed. The sidebar now shows the challenge view instead of verdict buttons over a live bot-gate. Match the specific challenges.cloudflare.com host only: the same page hosts a cf.clym-widget.net iframe (Clym consent, not Cloudflare) that a bare "cf"/"cloudflare" substring would false-positive on. The `!turnstile` guard is load-bearing, not stylistic — real solved Turnstile pages also carry this script, so without it every solved container-Turnstile would falsely re-detect; the guard routes the container case to the existing solved/unsolved logic. Verified against live QA: clean /results pages return null for this selector (no false positive), and solving /InternalCaptcha navigates away, so the fresh load reports CHALLENGE_RESOLVED with no in-page solved-state detection needed. Scoped minimal by design — the human-in-the-loop plus Skip is the backstop, so the unsolved-refinement and observer hardening are deferred until a broker whose gate resolves inline actually needs them. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/content/classify.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/content/classify.ts b/src/content/classify.ts index 0fc7799..776d478 100644 --- a/src/content/classify.ts +++ b/src/content/classify.ts @@ -24,6 +24,16 @@ export function detectChallenge(): boolean { return true; // standalone CF iframe (non-Turnstile challenge) } + // Explicitly-rendered Turnstile (turnstile.render(...)): no .cf-turnstile container and the widget + // iframes are about:blank, so neither branch above matches — but the Turnstile API script is in the + // top document. This is the TruePeopleSearch /InternalCaptcha managed-challenge shape. Match the + // specific host ONLY: a co-resident cf.clym-widget.net iframe (Clym consent, not Cloudflare) must + // not match a bare "cf"/"cloudflare" substring. The `!turnstile` guard defers the container case to + // the block above — a solved container-Turnstile still carries this script and must stay resolved. + if (!turnstile && document.querySelector('script[src*="challenges.cloudflare.com/turnstile"]')) { + return true; + } + // Other embedded CAPTCHA widgets. return [ 'iframe[src*="hcaptcha.com"]', From 0db9645081644dedde860d3289cf8cd1172c8fec Mon Sep 17 00:00:00 2001 From: Dustin VanKrimpen Date: Fri, 3 Jul 2026 22:59:26 -0400 Subject: [PATCH 3/3] docs(progress): carry M9 per-broker challenge-resolve gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detectChallenge()'s Turnstile-script signal (added in this branch) is generic and manifest-bounded, but its safety rests on the gate navigating away on solve — proven on TruePeopleSearch only (n=1). Record the per-broker onboarding gate in the M9 milestone and a code-facing pointer in the TODO table, so a future broker that resolves its gate inline gets a resolve signal before it can strand the challenge view over real results. Co-Authored-By: Claude Opus 4.8 (1M context) --- plan/expurge-progress.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plan/expurge-progress.md b/plan/expurge-progress.md index 22d90d5..5bb69ba 100644 --- a/plan/expurge-progress.md +++ b/plan/expurge-progress.md @@ -99,6 +99,7 @@ superseded by the sidebar architecture — see `sidebar-nav.md` for the current - ~25 verified people-search brokers in brokers.json (all channels personally verified, trust bits stamped) - Pre-launch verify: CCPA template legal language; DROP registry cross-reference (Q-010) - CI schema validator: rejects malformed records, enforces trust-bit hygiene (contributed records must be `trust: unverified`) +- **Per-broker challenge-resolve gate** (onboarding checklist item): for each new broker, confirm its bot-gate **navigates away on solve** (like TPS `/InternalCaptcha`). `detectChallenge()`'s Turnstile-script signal is generic and manifest-bounded (it only runs where `content_scripts.matches` injects), but resolve-safety is proven on TPS **only (n=1)**. If a broker instead resolves **inline** — results swap in place, URL unchanged, the `challenges.cloudflare.com/turnstile` `