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: 19 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New exports: `AgentVerifier`, `createAgentVerifier`, `DirectoryCache`, `DEFAULT_SIGNED_AGENT_DIRECTORIES`, and types `AgentVerdict`, `AgentStatus`, `AgentCategory`, `WebBotAuthConfig`, `AgentVerifierOptions`, `SignedAgentDirectory`.
- Doc: "Verify AI agents with Web Bot Auth in Next.js" (`docs/verify-ai-agents-web-bot-auth.md`).

## [0.10.0] - 2026-07-31

### Added
- **Honeytoken injection for Fastify.** Express and Next.js gained this in 0.8.x;
Fastify still generated a token and left you to place the link. The plugin now
injects a hidden trap link into HTML replies and arms the tripwire it points at.
- On by default when `apiKey` is set; `honeytoken: false` opts out.
- Injected in an `onSend` hook, so only full `text/html` replies are rewritten
and Fastify recomputes `Content-Length`.
- The token is derived from the API key, so every replica advertises and arms
the same path.
- **Streamed replies are not rewritten** — buffering a stream to insert an
anchor would discard the streaming behaviour the app asked for. The plugin
logs a warning once per process with the markup to embed manually, so the
gap is visible rather than silent.

## [0.9.0] - 2026-07-31

### Added
- **Bot classification in the request path** — rules can now act on who the
User-Agent says it is, which the AI-scraper docs described but nothing could do.
User-Agent says it is, synchronously and with no network call.
- `bots()` rule: `bots({ categories: ['training_crawler'] })`,
`bots({ ai: true, allow: ['perplexitybot'] })`,
`bots({ agents: ['gptbot', 'ClaudeBot'], action: 'THROTTLE' })`.
Expand All @@ -28,8 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New exports: `bots`, `BotRule`, `matchUserAgent`, `classifyUserAgent`,
`BOT_REGISTRY`, `BOT_CATEGORIES`, and types `BotVerdict`, `BotAgent`,
`BotCategory`, `BotRuleConfig`.
- 168 agents generated from the Go registry, matched locally with no network
call. Category names match the dashboard's `ai_scraper_category` column.
- 168 known agents, matched locally. Category names match the
`ai_scraper_category` values shown in your dashboard.
- `ai: true` covers training crawlers, AI search crawlers, AI agents and AI
assistants. It excludes `search_crawler` — blocking Googlebot would deindex
your site.
Expand Down
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.9.0",
"version": "0.10.0",
"description": "Web Decoy browser widget - signal collection, proof-of-work, and captcha UI",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/clearance-behavior.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @jest-environment jsdom
*
* Locks the behavioral-clearance contract (app repo #328, PRD FR10): what may
* Locks the behavioral-clearance contract : what may
* leave the browser, when a session is summarized at all, and that scripted
* input produces measurably different aggregates than a human hand.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/clearance-behavior.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Behavioral human-likelihood for clearance minting (app repo #328, PRD FR10).
* Behavioral human-likelihood for clearance minting .
*
* A wd_clearance token normally says only "this is a real browser that hasn't
* tripped deception". This module lets a session also present POSITIVE evidence
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/clearance.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @jest-environment jsdom
*
* Locks the wd_clearance device-fp contract (#128). The fp is the deny-list key,
* Locks the wd_clearance device-fp contract. The fp is the deny-list key,
* so its exact composition MUST stay byte-identical to the edge challenge page
* (app repo: edge/clearance-worker). This test pins the canonical string, a golden
* hash the worker can cross-assert, and the no-canvas/WebGL guarantee.
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/clearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ interface MintResponse {

/** Call the public issuance endpoint. Returns null on any failure (fail open).
* `behavior`, when present, carries the session's interaction aggregates and
* can earn the token a graded 'human-likely' trust level (#328). */
* can earn the token a graded 'human-likely' trust level. */
async function mint(
ingestUrl: string,
siteKey: string,
Expand Down Expand Up @@ -156,7 +156,7 @@ export interface ClearanceOptions {
scope?: string;
/**
* Collect interaction aggregates and upgrade the token to a graded
* 'human-likely' trust level once the visitor actually interacts (#328).
* 'human-likely' trust level once the visitor actually interacts.
* Default true. Set false to mint clean tokens only — routes that require a
* minimum trust level will then always challenge.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Exposes `window.WebDecoyCaptcha`, auto-initializes any `[data-webdecoy]`
* elements, and — given a `data-site-key` on the script tag — silently mints a
* wd_clearance cookie for real browsers (closes the #124 decoy → deny loop in
* wd_clearance cookie for real browsers (closes the decoy → deny loop in
* monitor/allow-and-observe mode). Deferred to idle; no page-load cost.
*
* <script src="webdecoy.global.js" data-site-key="ORG_ID"
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export { FormAnalyzer, getFormAnalyzer } from './collectors/form';
export { sha256 } from './sha256';
export { startClearance, computeDeviceFP, CLEARANCE_COOKIE, FP_VERSION, DEFAULT_INGEST_URL } from './clearance';
export type { ClearanceOptions, DeviceFPInputs } from './clearance';
// Behavioral human-likelihood (#328). Exported so the collection contract is
// Behavioral human-likelihood. Exported so the collection contract is
// inspectable by anyone integrating — summarizeBehavior IS the published list
// of what we send.
export { summarizeBehavior, hasEnoughInteraction, watchInteraction } from './clearance-behavior';
Expand Down
4 changes: 2 additions & 2 deletions packages/express/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@webdecoy/express",
"version": "0.9.0",
"version": "0.10.0",
"description": "Web Decoy middleware for Express.js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -40,7 +40,7 @@
"url": "https://github.com/WebDecoy/node/issues"
},
"dependencies": {
"@webdecoy/node": "^0.9.0"
"@webdecoy/node": "^0.10.0"
},
"peerDependencies": {
"express": "^4.18.0 || ^5.0.0"
Expand Down
4 changes: 2 additions & 2 deletions packages/express/src/honeytoken-injection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AddressInfo } from 'node:net';
import { webdecoy } from './middleware';

/**
* Honeytoken injection through a real Express app (#482).
* Honeytoken injection through a real Express app.
*
* The unit tests cover the injection function. These cover the part that can
* corrupt a customer's response: intercepting res.write/res.end. Each case here
Expand Down Expand Up @@ -160,7 +160,7 @@ describe('the injected link is actually armed', () => {
});

/**
* Streaming SSR (#482 follow-up).
* Streaming SSR.
*
* The first implementation required `!res.headersSent` before intercepting.
* Angular SSR — and Nuxt, and anything else that calls `res.writeHead()` then
Expand Down
23 changes: 9 additions & 14 deletions packages/express/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,22 @@ export interface WebDecoyMiddlewareOptions extends ProtectOptions {
* a rule governs that rule, not the score, and `onBlocked` did not receive
* `next`, so "record it and serve the request anyway" could not be expressed.
*
* Found by installing it on a live site that takes payments: the homepage
* returned `{"error":"Forbidden"}` on the first request, as did an ordinary
* `python-requests` user agent.
*
* Nobody adopts a defence by having it break their site on the first install.
* * Nobody adopts a defence by having it break their site on the first install.
* Watch what it would have done, then set `mode: 'enforce'`.
*/
mode?: 'monitor' | 'enforce';

/**
* Inject a hidden honeytoken link into HTML responses, and arm the tripwire it
* points at. Defaults to **on** when an apiKey is present (#482).
* points at. Defaults to **on** when an apiKey is present.
*
* The SDK used to generate a honeytoken and ask the developer to embed it.
* Almost nobody did: `sdk_tripwire` had FOUR rows in production, ever, while
* the WordPress plugin — which injects the link itself — has real coverage.
* Almost nobody does, and an unplaced link is a trap nothing can walk into.
*
* A trap hit is the only detection here that needs no score, no JavaScript, no
* fingerprint and no IP. It is also the only one that scores: honeypot signals
* are weighted 38% against user-agent's 1%, which is why every rule-less `sdk`
* detection ever recorded came out at 0.
* carry the highest weight in the threat score, while a User-Agent — all a
* rule-less install can otherwise report — carries nearly none.
*
* Set `false` to opt out, or place the link yourself for apps that stream.
*/
Expand Down Expand Up @@ -182,7 +177,7 @@ export function webdecoy(
const onBlocked = config.onBlocked || defaultOnBlocked;
const mode = config.mode ?? 'monitor';

// Honeytoken (#482). Derived from the API key so every replica computes the
// Honeytoken. Derived from the API key so every replica computes the
// same path without coordinating — a random per-process token would advertise
// a link whose tripwire only one replica had armed.
//
Expand Down Expand Up @@ -230,7 +225,7 @@ export function webdecoy(
metadata: config.metadata,
});

// Honeytoken injection (#482).
// Honeytoken injection.
//
// Buffers the body only for full HTML documents and rewrites it once. The
// guards below are not defensive padding — each one is a way this could
Expand Down Expand Up @@ -341,7 +336,7 @@ export function webdecoy(
if (result.allowed) {
// Attach detection info to request for downstream use
(req as any).webdecoy = result.detection;
// And what the edge validator said, typed (#481). A handler can branch on
// And what the edge validator said, typed. A handler can branch on
// req.webdecoyEdge.isScript instead of string-matching x-wd-class, and
// `present: false` tells it the edge was never in front of this request —
// which is no information, not a clean bill of health.
Expand Down Expand Up @@ -374,7 +369,7 @@ declare global {
detection_id: string;
rule_enforced: boolean;
};
/** What the edge validator said about this request (#481). */
/** What the edge validator said about this request. */
webdecoyEdge?: EdgeVerdict;
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/fastify/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'],
};
4 changes: 2 additions & 2 deletions packages/fastify/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@webdecoy/fastify",
"version": "0.9.0",
"version": "0.10.0",
"description": "Web Decoy plugin for Fastify",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -40,7 +40,7 @@
"url": "https://github.com/WebDecoy/node/issues"
},
"dependencies": {
"@webdecoy/node": "^0.9.0",
"@webdecoy/node": "^0.10.0",
"fastify-plugin": "^4.5.1"
},
"peerDependencies": {
Expand Down
Loading