Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.19.1] - 2026-10-15

### Security

- Improved detection-report privacy: recognized credential headers are reported by name with their values withheld. WebDecoy clearance-cookie support is preserved, and local rules and characteristics continue to use the original request.

## [0.19.0] - 2026-10-12

### Added
- 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.
- 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.
- 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.

## [0.18.5] - 2026-10-12

### Fixed

- **Browser clearance keeps renewing during long sessions.** The refresh callback previously returned while the cookie was present and scheduled no successor; starting with an existing cookie also scheduled no renewal. `startClearance()` now keeps one lifecycle per page, renews before expiry, notices cookie changes from another tab, and checks again when the page becomes visible. Repeated startup and behavioral upgrades share requests and cached fingerprint collection.
- **Clearance mint failures are bounded.** Requests time out after 10 seconds and transient failures back off from one minute to five minutes. An explicit server refusal stops automatic attempts on that page. Cookie expiry is only a bounded scheduling hint; server and edge verification remain authoritative.
- **Accepted behavioral aggregates remain on renewal requests.** The server re-evaluates the evidence on each mint. Browser clearance still does not intercept or automatically replay application fetch/XHR requests.


## [0.18.4] - 2026-10-02

### Fixed
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,16 @@ if (!result.allowed) {
| [@webdecoy/hono](https://www.npmjs.com/package/@webdecoy/hono) | [![npm](https://img.shields.io/npm/v/@webdecoy/hono.svg)](https://www.npmjs.com/package/@webdecoy/hono) | Hono middleware (Workers, Bun, Deno) |
| [@webdecoy/client](https://www.npmjs.com/package/@webdecoy/client) | [![npm](https://img.shields.io/npm/v/@webdecoy/client.svg)](https://www.npmjs.com/package/@webdecoy/client) | Browser-side signal collector |

## Browser session clearance

`@webdecoy/client` 0.18.5 or later keeps browser clearance renewing during long
sessions, including startup with an existing cookie. It coalesces repeated
startup, bounds mint failures with a timeout and backoff, and stops automatic
attempts when the server refuses clearance. Application fetch/XHR requests are
not intercepted or replayed globally. Version 0.19.0 adds an opt-in
`createClearanceFetch()` helper for one GET/HEAD retry through your own clearance
flow; mutations and policy refusals never recover automatically. See the [browser client guide](packages/client/README.md#session-clearance).

## One bot policy, published and enforced

`botPolicy()` produces both the `robots.txt` you publish and the rule that
Expand Down
11 changes: 11 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ dashboard.
- `@webdecoy/client` — browser signal collector and proof-of-work captcha
- `@webdecoy/node/testing` — test-suite helpers, offline by default

Browser session clearance (`@webdecoy/client` 0.18.5+): call
`startClearance({ siteKey: 'YOUR_PUBLISHABLE_ORGANIZATION_ID' })`. Startup is
deferred to idle; fresh cookies are reused and renewed before expiry. Repeated
startup shares one lifecycle per page. Mint requests time out after 10 seconds;
transient failures back off from one minute to five minutes. Explicit refusal
stops automatic attempts on that page. Cookie expiry is only a scheduling hint;
server/edge verification remains authoritative. The client does not intercept
or replay application fetch/XHR requests. See packages/client/README.md.

For Bun, Deno, Astro, Nitro, SvelteKit or Remix use `createFetchGuard()` from
`@webdecoy/node` directly — no adapter package needed.

Expand Down Expand Up @@ -138,3 +147,5 @@ Offline by default: an API key in the environment is ignored unless
- Web Bot Auth guide: https://github.com/WebDecoy/node/blob/main/docs/verify-ai-agents-web-bot-auth.md
- Dashboard: https://app.webdecoy.com
- Docs: https://docs.webdecoy.com

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`.
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 96 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,31 @@ const result = await WebDecoyCaptcha.execute('pk_live_...', { action: 'login' })
if (result.success) { /* result.token */ }
```

## Session clearance

```js
import { startClearance } from '@webdecoy/client';

startClearance({ siteKey: 'YOUR_PUBLISHABLE_ORGANIZATION_ID' });
```

With version **0.18.5 or later**, the client defers initial minting to browser
idle, reuses fresh `wd_clearance` cookies, and renews before expiry throughout
long sessions. Repeated startup shares one lifecycle and collected fingerprint
per page. Cookie changes from another tab are noticed during periodic checks;
returning to a visible page checks clearance again.

Mint requests time out after 10 seconds. Transient failures retry with backoff
from one minute to five minutes; explicit server refusal stops automatic
attempts on that page. Unsigned expiry is only a scheduling hint. The server and
edge validator continue to verify every token and enforce revocation.

The cookie is Secure, SameSite Lax and host-only. Accepted interaction aggregates
remain on renewal requests, and the server evaluates them again; pass
`behavior: false` to disable the behavioral upgrade. The client does not intercept
or retry application fetch/XHR requests globally. An edge API challenge is a JSON
403; the opt-in helper below handles safe reads through your application’s flow.

## What it collects

| Group | Signals |
Expand Down Expand Up @@ -106,3 +131,74 @@ and Next.js adapters and the full detection/scoring reference.
## License

MIT

## API challenge recovery

With `@webdecoy/client` **0.19.0 or later** and an updated edge validator, opt in
for the API reads your application wants to recover:

```ts
import { createClearanceFetch } from '@webdecoy/client';
import { earnClearance } from './clearance-flow'; // your application's flow

const fetchProtected = createClearanceFetch({
origin: location.origin,
timeoutMs: 10_000,
recover: ({ origin, challenge, signal }) =>
earnClearance({ origin, reason: challenge.reason, signal }),
onEvent: event => console.debug(event.type, event.status),
});

const controller = new AbortController();
const response = await fetchProtected('/api/catalog', {
credentials: 'same-origin',
signal: controller.signal, // your application's AbortController
});
// Handle response.ok/status/body just as with native fetch.
```

`earnClearance` is application code, not an SDK export. It must earn and install
the host's `wd_clearance` cookie through your configured issuance/verification
flow and return `true` only when ready to retry. `startClearance()` returns
`void`; awaiting it does not wait for a token. A self-hosted captcha widget token
is not a clearance cookie. A stronger-grade response needs a flow that earns the
route's required grade; simply replacing a token cannot grant that grade.

The helper retries **GET/HEAD once** after a recognized recoverable challenge.
Concurrent calls through one adapter share one recovery callback for its
configured origin; that callback receives the first challenge's reason. Use one
adapter for each origin/recovery flow. The edge evaluates every retry again.
POST/PATCH/PUT/DELETE and other methods pass through once with their original
body. There is no mutation replay opt-in and no XHR interception. The standalone
script exposes the same helpers at `window.WebDecoyClearance`.

Recovery and the retry share a deadline after the first challenge: 10 seconds by
default, configurable from 1 ms to 60 seconds. Refusal, resolver rejection,
offline state, or deadline expiry returns the original readable challenge
response. Caller cancellation and fetch network errors reject as native fetch
does. One cancelled caller does not cancel other callers waiting for the shared
flow; when none remain, the recovery signal aborts. Your flow must stop network
work and close its UI on that signal. If it ignores cancellation, later calls
remain attached to that stopped attempt until it settles, rather than opening
overlapping recovery dialogs. Browsers without `AbortSignal.any()` return the
challenge without attempting recovery.

Only version-1 responses with the exact WebDecoy challenge headers qualify.
Ordinary application 403s, unsupported/malformed contracts, revoked sessions,
crawler-policy refusals and agent impersonation do not trigger recovery.
Redirected responses, different origins and `credentials: 'omit'` also do not.
The helper preserves request options and leaves response bodies/streams unread.
It adds no request headers or CORS permission. Cross-origin calls need
`credentials: 'include'`, the protected host's cookie, and an application-approved
CORS policy that exposes the four challenge headers; otherwise the browser
cannot identify the response. A cookie set on the page's host does not clear a
different API host.

`onEvent` reports `challenge-served`, `recovery-attempted`,
`recovery-succeeded`/`recovery-failed`, and the retried `enforcement-outcome`
(status). Attempts are per waiting request; count callback invocations separately
for shared flows. Success means the retry returned an OK response, not a new
trust grade. These local observations send no telemetry automatically and must
remain distinct from the validator's verdict counts. Observation failures cannot
change the request. Interactive application flows must provide keyboard access,
focus restoration, an announced status and cancellation; the helper renders no UI.
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@webdecoy/client",
"version": "0.18.4",
"version": "0.19.1",
"description": "Web Decoy browser widget - signal collection, proof-of-work, and captcha UI",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
Loading
Loading