Skip to content

feat(types,provider): bucket context-aware entropy by device type - #3120

Merged
forgetso merged 3 commits into
mainfrom
feat/device-type-context-entropy
Sep 1, 2026
Merged

feat(types,provider): bucket context-aware entropy by device type#3120
forgetso merged 3 commits into
mainfrom
feat/device-type-context-entropy

Conversation

@HughParry

Copy link
Copy Markdown
Contributor

Companion to prosopo/captcha-private#4243 (context entropy sweep). That PR
computes the baselines; this one decides what a baseline is for and how the
decision machine looks one up. Merge this first — the private PR imports
deviceTypeFromUserAgent and the new ContextType members.

Why

Context-aware validation compares a session's head SimHash against a baseline
for its context. That context was default | webview, which puts a phone and
a desktop in the same bucket. Those emit genuinely different <head>s —
responsive markup, different injected scripts — so the blended baseline
matches neither well, and a threshold tight enough to catch a bot on desktop
starts rejecting real phones.

Contexts are now the device family crossed with the webview flag:

browser webview
desktop desktop desktop-webview
mobile mobile mobile-webview
tablet tablet tablet-webview

desktop-webview is in there deliberately. Desktop webviews are a real and
notably fraudulent population on this platform (see the Twickets
desktop-webview rules and the twickets_desktop_webview_impossible list), so
folding them into plain desktop would let exactly the traffic we want
excluded define what "normal desktop" looks like.

Classification

deviceTypeFromUserAgent in @prosopo/types is a small dependency-free UA
classifier. Deliberately not ua-parser-js: this module is imported by the
browser widget bundles, and the off-provider entropy sweep has to bucket
stored sessions identically to the decision machine or it writes baselines
nothing ever looks up. One shared function with no runtime dependency is what
keeps the two sides in lockstep.

Tablets are matched before phones because both tablet shapes collide with the
phone patterns: an iPad's UA carries a Mobile/<build> token, and an Android
tablet is exactly "Android without Mobile".

Known gap, documented at the call site: an iPadOS 13+ Safari in its default
desktop mode identifies as a Mac and lands in desktop. Nothing in a UA
separates it from a real Mac. It's left there deliberately — both sides make
the same call, which is what actually matters for the lookup to line up.

Back-compat

default and webview remain valid ContextType members, so settings
already stored against them keep parsing (verified: zod's record-with-enum-key
validates present keys only, and rejects unknown ones). expandContexts maps
a legacy default onto the three non-webview families and a legacy webview
onto the three webview families, at the threshold they were saved with; an
explicit device entry always wins over the legacy entry covering it, so a
customer can tighten one family without restating the rest.

Nothing downstream of settings parsing branches on the legacy keys, and no
data migration is required
.

Behaviour change

A request whose context is not configured now skips context validation
rather than borrowing another context's baseline. Previously, configuring a
single context validated every request against it — harmless with two
contexts, but with six it would measure desktop traffic against a tablet
baseline and reject real users wholesale. isContextConfigured is the new
guard, and determineContextType now takes the raw request UA alongside the
webview flag.

New site-key registrations default to all six device contexts.

Tests

Provider unit suite: 73 files pass. contextAwareValidation.unit.test.ts is
rewritten for the new API (15 cases: per-family classification, the webview
cross, legacy expansion, explicit-over-legacy precedence). The three
getFrictionlessCaptchaChallenge context-selection cases are rewritten to
assert the lookup follows the request's UA and webview flag, that a legacy
default expands across the non-webview families, and that an unconfigured
context skips the stage. @prosopo/types gains coverage for the classifier
(six real UA fixtures), the device x webview cross, expandContexts, and
schema round-trips for both legacy and device contexts.

🤖 Generated with Claude Code

https://claude.ai/code/session_017pFGMWxTGoqe2fBe54TYLx

Context-aware validation compared a session's head SimHash against a
baseline for `default | webview`, which puts a phone and a desktop in the
same bucket. Those emit genuinely different `<head>`s, so the blended
baseline matched neither well.

Contexts are now device family x webview: desktop, desktop-webview,
mobile, mobile-webview, tablet, tablet-webview. desktop-webview is
included deliberately — desktop webviews are a real and notably
fraudulent population here, and folding them into the plain desktop
baseline would let exactly the traffic we want excluded define what
"normal desktop" looks like.

`deviceTypeFromUserAgent` is a dependency-free UA classifier rather than
ua-parser-js: this module is imported by the browser bundles, and the
off-provider entropy sweep must bucket stored sessions identically or it
writes baselines the decision machine never looks up.

`default` and `webview` stay valid ContextType members so stored
settings keep parsing; `expandContexts` maps them onto their device
families at the saved threshold, with explicit device entries winning.
No data migration needed.

Behaviour change: a request whose context is unconfigured now skips
context validation rather than borrowing another context's baseline.
With six contexts, applying one baseline to every request would reject
real users wholesale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pFGMWxTGoqe2fBe54TYLx
@github-actions

Copy link
Copy Markdown
Contributor

No updates since 2026-08-24T19:51:44Z (over 1 days), so this PR has been converted to draft. That stops it holding CI runners.

Nothing is lost — gh pr ready 3120, or the "Ready for review" button, picks it straight back up. Add the keep-ready label to exempt it permanently.

Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely.

@github-actions

Copy link
Copy Markdown
Contributor

No updates since 2026-08-26T00:03:58Z (over 1 days), so this PR has been converted to draft. That stops it holding CI runners.

Nothing is lost — gh pr ready 3120, or the "Ready for review" button, picks it straight back up. Add the keep-ready label to exempt it permanently.

Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely.

@github-actions

Copy link
Copy Markdown
Contributor

No updates since 2026-08-27T01:06:35Z (over 1 days), so this PR has been converted to draft. That stops it holding CI runners.

Nothing is lost — gh pr ready 3120, or the "Ready for review" button, picks it straight back up. Add the keep-ready label to exempt it permanently.

Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely.

@github-actions

Copy link
Copy Markdown
Contributor

No updates since 2026-08-28T02:28:28Z (over 1 days), so this PR has been converted to draft. That stops it holding CI runners.

Nothing is lost — gh pr ready 3120, or the "Ready for review" button, picks it straight back up. Add the keep-ready label to exempt it permanently.

Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely.

@forgetso
forgetso enabled auto-merge (squash) September 1, 2026 09:35
@forgetso
forgetso merged commit a62b994 into main Sep 1, 2026
12 checks passed
@forgetso
forgetso deleted the feat/device-type-context-entropy branch September 1, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants