Skip to content

Friction

phil/bot-detection-bundle

A verdict is not a refusal. A verdict is computed on the client, so it can be forged, and a false positive must still get through. The strongest outcome of a verdict is therefore one more step of friction.

State: Friction and FrictionPolicy are implemented and tested.

Friction

A pure enum with exactly two cases:

Caseadds()Meaning
Friction::NonefalseThe request proceeds as usual
Friction::SteptrueThe request gets one more step of friction

There is deliberately no Refuse case, and the tests assert that Friction::cases() is exactly [None, Step].

The application gives Step its meaning on each surface, for example:

  • one more step of back-off on a sign-up rate limiter;
  • a forced stronger authentication challenge, or one more step on a payment-attempt limiter;
  • a hit that audience measurement does not count.

The step never records why it was added. It melts into the state of whatever it slows down (a limiter's counter, a challenge requirement), and that state does not keep the verdict.

FrictionPolicy

Maps a verdict to friction. The mapping is fixed by design except for one choice:

VerdictrobotOnly()robotOrUnknown()
HumanNoneNone
RobotStepStep
UnknownNoneStep
$policy = FrictionPolicy::robotOrUnknown();
$policy->unknownAddsFriction;              // true
$policy->frictionFor(Verdict::Unknown);    // Friction::Step

FrictionPolicy::robotOnly()->frictionFor(Verdict::Unknown); // Friction::None

The constructor is private: the two named constructors are the only policies.

Choosing, surface by surface

PolicyCostUse when
robotOnly()A client can skip detection by sending nothingFriction on a person is expensive (a payment), or the browser code is not deployed on the surface yet
robotOrUnknown()Slows clients that never ran the detection script, and also people whose browser blocks scriptsAbuse is cheap and friction mild (a sign-up back-off)

On a surface where the verdict controller is not placed, every verdict is Unknown: robotOrUnknown() then adds a step to every request there.

What the policy does not decide

A browser that resists fingerprinting is not a robot. Randomized or blanked signals, as privacy-focused browsers produce them, must not trip a detector. That is a property of the detectors in the browser, not of this policy: by the time a verdict reaches the server, it is already robot or human (browser-signals.md).

Declared crawlers (good bots announcing themselves in their user agent) are a server-side matter, decided from the user agent with a crawler list such as jaybizzle/crawler-detect, not by this bundle.