Skip to content

Commit 8f498e4

Browse files
authored
Add opt-in bounded API clearance recovery for browser reads (#64)
1 parent aaf4acb commit 8f498e4

15 files changed

Lines changed: 477 additions & 24 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.19.0] - 2026-10-12
11+
12+
### Added
13+
- Explicit `createClearanceFetch()` adapter and `readClearanceChallenge()` parser for the version-1 edge API challenge contract. Available as library exports and `window.WebDecoyClearance` in the standalone bundle.
14+
- GET/HEAD recovery with one retry, shared recovery per adapter/protected origin, a bounded deadline, cancellation, and local observation hooks. Applications provide their own clearance flow; refusal remains server-owned.
15+
- Policy refusals, ordinary origin 403s, unsupported contracts, omitted credentials, redirects, offline state, and all mutations do not trigger automatic recovery. No global fetch interception or XHR handling.
16+
1017
## [0.18.5] - 2026-10-12
1118

1219
### Fixed

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,9 @@ if (!result.allowed) {
250250
sessions, including startup with an existing cookie. It coalesces repeated
251251
startup, bounds mint failures with a timeout and backoff, and stops automatic
252252
attempts when the server refuses clearance. Application fetch/XHR requests are
253-
not intercepted or replayed. See the [browser client guide](packages/client/README.md#session-clearance).
253+
not intercepted or replayed globally. Version 0.19.0 adds an opt-in
254+
`createClearanceFetch()` helper for one GET/HEAD retry through your own clearance
255+
flow; mutations and policy refusals never recover automatically. See the [browser client guide](packages/client/README.md#session-clearance).
254256

255257
## One bot policy, published and enforced
256258

‎llms.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,5 @@ Offline by default: an API key in the environment is ignored unless
147147
- Web Bot Auth guide: https://github.com/WebDecoy/node/blob/main/docs/verify-ai-agents-web-bot-auth.md
148148
- Dashboard: https://app.webdecoy.com
149149
- Docs: https://docs.webdecoy.com
150+
151+
API recovery (`@webdecoy/client` 0.19.0+): `createClearanceFetch({ origin, recover, timeoutMs?, onEvent? })` returns an explicit fetch adapter. `recover({ origin, challenge, signal })` must earn/install the protected origin's clearance cookie and resolve true; `startClearance()` is not awaitable recovery. Exact v1 challenge headers, GET/HEAD only, one retry, shared callback per adapter, 10s default/60s max recovery-and-retry deadline. Refusal/timeout returns the original response; caller abort/network errors reject. Mutations, ordinary 403, revoked/crawler-refused/agent-impersonation, redirects and omitted credentials never recover. Cross-origin needs include credentials, host cookie and approved CORS exposing the challenge headers. No global interception, XHR support or automatic telemetry. Standalone global: `window.WebDecoyClearance`.

‎package-lock.json‎

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/client/README.md‎

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ edge validator continue to verify every token and enforce revocation.
8787
The cookie is Secure, SameSite Lax and host-only. Accepted interaction aggregates
8888
remain on renewal requests, and the server evaluates them again; pass
8989
`behavior: false` to disable the behavioral upgrade. The client does not intercept
90-
or retry application fetch/XHR requests: an edge API challenge is a JSON 403
91-
that your application must handle explicitly.
90+
or retry application fetch/XHR requests globally. An edge API challenge is a JSON
91+
403; the opt-in helper below handles safe reads through your application’s flow.
9292

9393
## What it collects
9494

@@ -131,3 +131,74 @@ and Next.js adapters and the full detection/scoring reference.
131131
## License
132132

133133
MIT
134+
135+
## API challenge recovery
136+
137+
With `@webdecoy/client` **0.19.0 or later** and an updated edge validator, opt in
138+
for the API reads your application wants to recover:
139+
140+
```ts
141+
import { createClearanceFetch } from '@webdecoy/client';
142+
import { earnClearance } from './clearance-flow'; // your application's flow
143+
144+
const fetchProtected = createClearanceFetch({
145+
origin: location.origin,
146+
timeoutMs: 10_000,
147+
recover: ({ origin, challenge, signal }) =>
148+
earnClearance({ origin, reason: challenge.reason, signal }),
149+
onEvent: event => console.debug(event.type, event.status),
150+
});
151+
152+
const controller = new AbortController();
153+
const response = await fetchProtected('/api/catalog', {
154+
credentials: 'same-origin',
155+
signal: controller.signal, // your application's AbortController
156+
});
157+
// Handle response.ok/status/body just as with native fetch.
158+
```
159+
160+
`earnClearance` is application code, not an SDK export. It must earn and install
161+
the host's `wd_clearance` cookie through your configured issuance/verification
162+
flow and return `true` only when ready to retry. `startClearance()` returns
163+
`void`; awaiting it does not wait for a token. A self-hosted captcha widget token
164+
is not a clearance cookie. A stronger-grade response needs a flow that earns the
165+
route's required grade; simply replacing a token cannot grant that grade.
166+
167+
The helper retries **GET/HEAD once** after a recognized recoverable challenge.
168+
Concurrent calls through one adapter share one recovery callback for its
169+
configured origin; that callback receives the first challenge's reason. Use one
170+
adapter for each origin/recovery flow. The edge evaluates every retry again.
171+
POST/PATCH/PUT/DELETE and other methods pass through once with their original
172+
body. There is no mutation replay opt-in and no XHR interception. The standalone
173+
script exposes the same helpers at `window.WebDecoyClearance`.
174+
175+
Recovery and the retry share a deadline after the first challenge: 10 seconds by
176+
default, configurable from 1 ms to 60 seconds. Refusal, resolver rejection,
177+
offline state, or deadline expiry returns the original readable challenge
178+
response. Caller cancellation and fetch network errors reject as native fetch
179+
does. One cancelled caller does not cancel other callers waiting for the shared
180+
flow; when none remain, the recovery signal aborts. Your flow must stop network
181+
work and close its UI on that signal. If it ignores cancellation, later calls
182+
remain attached to that stopped attempt until it settles, rather than opening
183+
overlapping recovery dialogs. Browsers without `AbortSignal.any()` return the
184+
challenge without attempting recovery.
185+
186+
Only version-1 responses with the exact WebDecoy challenge headers qualify.
187+
Ordinary application 403s, unsupported/malformed contracts, revoked sessions,
188+
crawler-policy refusals and agent impersonation do not trigger recovery.
189+
Redirected responses, different origins and `credentials: 'omit'` also do not.
190+
The helper preserves request options and leaves response bodies/streams unread.
191+
It adds no request headers or CORS permission. Cross-origin calls need
192+
`credentials: 'include'`, the protected host's cookie, and an application-approved
193+
CORS policy that exposes the four challenge headers; otherwise the browser
194+
cannot identify the response. A cookie set on the page's host does not clear a
195+
different API host.
196+
197+
`onEvent` reports `challenge-served`, `recovery-attempted`,
198+
`recovery-succeeded`/`recovery-failed`, and the retried `enforcement-outcome`
199+
(status). Attempts are per waiting request; count callback invocations separately
200+
for shared flows. Success means the retry returned an OK response, not a new
201+
trust grade. These local observations send no telemetry automatically and must
202+
remain distinct from the validator's verdict counts. Observation failures cannot
203+
change the request. Interactive application flows must provide keyboard access,
204+
focus restoration, an announced status and cancellation; the helper renders no UI.

‎packages/client/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@webdecoy/client",
3-
"version": "0.18.5",
3+
"version": "0.19.0",
44
"description": "Web Decoy browser widget - signal collection, proof-of-work, and captcha UI",
55
"main": "./dist/index.js",
66
"module": "./dist/index.mjs",

0 commit comments

Comments
 (0)