Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions plan/expurge-progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` `<script>` persists — the detector would strand the challenge view over real results → forced Skip → missed hit. Before enabling such a broker, add a resolve signal (URL-path check or solved-token) or an option-2 per-broker `challenge` hint, plus a challenge fixture in `classify.test.ts`. (Origin: `fix/challenge-detection-managed`, 2026-07-03; the human-in-the-loop + Skip is the backstop that keeps a miss recoverable, not silent.)
- Optional stamp helper: `verify <broker-id> <channel>` CLI sets last_checked / verified_by / trust
- Full run on real brokers, bugs fixed
- AMO submission prep: screenshots, description, privacy notice, data-practices declaration
Expand All @@ -125,6 +126,7 @@ superseded by the sidebar architecture — see `sidebar-nav.md` for the current
| `src/background/index.ts` | AKA parsing splits on first space only — "Mary Jane Smith" gives first="Mary", last="Jane Smith"; smarter parsing (e.g. last-space split) deferred |
| `src/options/index.ts` | Settings section has no import JSON (export only); import deferred to M8 alongside persistence opt-ins |
| `src/background/index.ts` | `webNavigation` is declared in manifest permissions but `browser.webNavigation.onErrorOccurred` is not yet wired — add when M9 broker set makes load-error detection meaningful |
| `src/content/classify.ts` | Turnstile-script detection assumes solve **navigates away** (proven on TPS only). A broker that resolves the gate **inline** would strand the challenge view — see the M9 per-broker challenge-resolve gate before onboarding one |

---

Expand Down
27 changes: 27 additions & 0 deletions src/content/classify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
'<div class="cf-turnstile"></div><input name="cf-turnstile-response" value="tok">';
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);
});
});
10 changes: 10 additions & 0 deletions src/content/classify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]',
Expand Down
Loading