feat(types,provider): bucket context-aware entropy by device type - #3120
Conversation
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
|
No updates since Nothing is lost — Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely. |
|
No updates since Nothing is lost — Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely. |
|
No updates since Nothing is lost — Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely. |
|
No updates since Nothing is lost — Converting to draft disables auto-merge, so PRs with auto-merge enabled are excluded from this entirely. |
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
deviceTypeFromUserAgentand the newContextTypemembers.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 anda 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:
desktopdesktop-webviewmobilemobile-webviewtablettablet-webviewdesktop-webviewis in there deliberately. Desktop webviews are a real andnotably fraudulent population on this platform (see the Twickets
desktop-webview rules and the
twickets_desktop_webview_impossiblelist), sofolding them into plain
desktopwould let exactly the traffic we wantexcluded define what "normal desktop" looks like.
Classification
deviceTypeFromUserAgentin@prosopo/typesis a small dependency-free UAclassifier. 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 Androidtablet 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 UAseparates 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
defaultandwebviewremain validContextTypemembers, so settingsalready stored against them keep parsing (verified: zod's record-with-enum-key
validates present keys only, and rejects unknown ones).
expandContextsmapsa legacy
defaultonto the three non-webview families and a legacywebviewonto 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.
isContextConfiguredis the newguard, and
determineContextTypenow takes the raw request UA alongside thewebview flag.
New site-key registrations default to all six device contexts.
Tests
Provider unit suite: 73 files pass.
contextAwareValidation.unit.test.tsisrewritten for the new API (15 cases: per-family classification, the webview
cross, legacy expansion, explicit-over-legacy precedence). The three
getFrictionlessCaptchaChallengecontext-selection cases are rewritten toassert the lookup follows the request's UA and webview flag, that a legacy
defaultexpands across the non-webview families, and that an unconfiguredcontext skips the stage.
@prosopo/typesgains coverage for the classifier(six real UA fixtures), the device x webview cross,
expandContexts, andschema round-trips for both legacy and device contexts.
🤖 Generated with Claude Code
https://claude.ai/code/session_017pFGMWxTGoqe2fBe54TYLx