feat(sdk): join the browser's signals to the requests that follow - #33
Merged
Conversation
@webdecoy/client collected behavioural, environmental and form signals,
DetectionEngine scored them, and /score returned a verdict -- to the
browser, which then forgot it. The origin never learned anything from the
submission. Every part existed and nothing joined them, so the README
mentioned the client package in one table row and never explained a path.
That gap matters because this is the answer to the one thing a tripwire
cannot catch: a Playwright-driven Chrome that browses only the links a
human would. It has a genuine fingerprint, follows no hidden links and
requests no honeypot paths -- and it still cannot fake having a person
behind it.
createCaptchaEndpoints({ signalStore }) now records the verdict against
the browser's session and echoes the session id so the widget can set the
cookie the rule reads. clientSignals({ store }) acts on it.
A request with no session is NOT_RUN, never a denial. curl and Googlebot
both send nothing, and scoring silence would deny exactly the crawlers we
most need to keep -- which is also why the guide says to start in dryRun:
this is the one rule in the SDK that is a judgement rather than a fact.
The guide points at the npm package rather than a CDN URL. A versionless
script tag with no integrity attribute is a supply-chain dependency on
whoever is serving it, and I had no verified CDN path for this bundle
anyway.
Closes WebDecoy/app#733
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #733.
The gap
@webdecoy/clientcollected behavioural, environmental and form signals.DetectionEnginescored them./scorereturned a verdict — to the browser, which then forgot it. The origin never learned anything from the submission. Joining the two was left as an exercise for the developer, so in practice nobody did, and the README mentioned the client package in one table row and never explained a path.That's worth closing because it's the SDK's answer to the one thing a tripwire structurally cannot catch: a Playwright-driven Chrome that browses only the links a human would. Genuine fingerprint, no hidden links followed, no honeypot paths requested — invisible to intent-based detection. What it can't fake is having a person behind it.
The wiring
/scorerecords the verdict against the browser'ssessionIdand echoes it back so the widget can set thewd_cscookie the rule reads — without that echo the developer would need a second endpoint to hand the browser its own session id.The store follows the same sync/async split as the rate-limit store, for the same reason: an in-memory default must not make every rule evaluation a promise. Async stores resolve through the
prepare()hook.The design decision worth reviewing
A request with no session is
NOT_RUN, never a denial.curl sends nothing. Googlebot sends nothing. A user with JavaScript disabled sends nothing. Treating absence as suspicion would deny exactly the crawlers a customer most needs to keep, and it would do it silently. So absence is "we have no information", which is the same stance
readEdgeVerdict()takes aboutpresent: false.Relatedly, the guide leads with
dryRun: true. This is the one rule in the SDK that produces a judgement rather than a fact — a tripwire hit is proof, a score is not — and that difference should shape how it's adopted.One thing I changed mid-write
The guide originally showed a
<script src="https://cdn.webdecoy.com/client/…">tag. Two problems: nointegrity/crossorigin, and I had no verified CDN path for that bundle. Made-up URLs in docs are worse than no URL, so it points at the npm package, with a note about serving the global bundle from your own origin or pinning it.Verification
16 new tests, 393 total. The end-to-end one drives a real
/scoresubmission with headless-Chrome-shaped signals, asserts the store recorded it, then asserts the next request through the SDK is denied on that basis — which is precisely the join that didn't exist.