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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@

[Quick start](#quick-start--no-api-key) · [Choose your framework](#choose-your-framework) · [Verify a detection](#verify-your-first-detection) · [SDK documentation](https://docs.webdecoy.com/sdk-plugins/node-sdk/?utm_source=github&utm_medium=readme&utm_campaign=node_sdk_install) · [Examples](#examples)

## Try it in your browser

**[Open the Express playground in StackBlitz](https://stackblitz.com/github/WebDecoy/node/tree/main/playgrounds/express?file=app.mjs)** · **[Watch the 30-second demo](https://youtu.be/4jeuH3tKeos)**

Send an ordinary request, then a decoy request. Inspect the actual HTTP response
and local tripwire verdict. Start in monitor mode; enable enforcement explicitly
in the playground to compare the response. No WebDecoy account or API key required.
StackBlitz runs the standalone Express example in a browser Node.js runtime.
See the [playground source and local alternative](playgrounds/express/README.md).

[![Watch the WebDecoy Express demo: send a request and inspect the decision](docs/assets/node-demo.png)](https://youtu.be/4jeuH3tKeos)

## What you can do

| Need | Start here |
Expand Down
Binary file added docs/assets/node-demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"test": "turbo run test && npm run test:playground",
"clean": "turbo run clean && rm -rf node_modules",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"check:edge": "npm run check:edge -w @webdecoy/node -w @webdecoy/nextjs -w @webdecoy/hono"
"check:edge": "npm run check:edge -w @webdecoy/node -w @webdecoy/nextjs -w @webdecoy/hono",
"test:playground": "npm ci --prefix playgrounds/express --ignore-scripts --no-audit --no-fund && npm test --prefix playgrounds/express"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/webdecoy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ TypeScript bot detection and protection for Node.js and edge runtimes. Run honey

Full documentation: [docs.webdecoy.com/sdk-plugins/node-sdk](https://docs.webdecoy.com/sdk-plugins/node-sdk/). Copy-paste install snippets for every stack: [docs.webdecoy.com/installation/install-with-ai](https://docs.webdecoy.com/installation/install-with-ai/).

**[Try the Express playground in your browser](https://stackblitz.com/github/WebDecoy/node/tree/main/playgrounds/express?file=app.mjs)** · [Watch the 30-second demo](https://youtu.be/4jeuH3tKeos)

## Using a framework? Install its adapter

Most apps should install a framework adapter alongside this package. The adapters handle request mapping, client IP, monitor mode and honeytoken injection for you. Install `@webdecoy/node` too: it is where `tripwire()`, `rateLimit()` and the other rules are imported from.
Expand Down
3 changes: 3 additions & 0 deletions playgrounds/express/.stackblitzrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"startCommand": "npm run dev"
}
48 changes: 48 additions & 0 deletions playgrounds/express/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# WebDecoy Express playground

[Open in StackBlitz](https://stackblitz.com/github/WebDecoy/node/tree/main/playgrounds/express?file=app.mjs)
· [Watch the 30-second demo](https://youtu.be/4jeuH3tKeos)

Send a real Express request, inspect the SDK's tripwire decision, and compare
monitoring with enforcement. No WebDecoy account or API key required. Browser
runtime startup and dependency installation may take a moment.

1. Open the preview and click **Send ordinary request**: HTTP200, `ALLOW`.
2. Click **Send decoy request**: HTTP404, `DENY`, `tripwire: true`.
3. Check **Enable enforcement for this demo**, then send the decoy again: HTTP403.
4. Send the ordinary request again: HTTP200.

The monitor response comes from our demo route; the enforce response comes from
the real SDK before that route runs. Status alone is not a detection receipt.

## Run locally

Requires Node.js 20+ and npm. Download this folder (it is standalone) or, from a
repository checkout:

```bash
cd playgrounds/express
npm ci --workspaces=false
npm run dev
```

Open `http://127.0.0.1:3000`. Run `npm test` for the real HTTP checks. The folder
pins published SDK packages and has its own lockfile, with no monorepo imports or
build step. If browser restrictions prevent WebContainers from starting, use
this local path; see [StackBlitz browser support](https://developer.stackblitz.com/platform/webcontainers/browser-support).

## What to bring to your app

See `app.mjs`: import `webdecoy` and `tripwire`, then mount middleware before your
routes. The two routers isolate the playground's monitor and enforce fixtures;
your real app should start with monitor mode. The preview renders actual JSON
responses with `textContent`; it does not generate synthetic SDK verdicts.

This is a controlled path-matching example. A hit does not establish malicious
intent: legitimate scanners and prefetchers may fetch decoys. Exposed verdicts
are for this demo; keep operational details server-side in your own app. This
keyless example does not send detections to the hosted dashboard or verify AI
agent identities.

[Full Express setup](https://docs.webdecoy.com/sdk-plugins/express/)
· [Separate AI-agent verification guide](https://github.com/WebDecoy/node/blob/main/docs/verify-ai-agents-web-bot-auth.md)
35 changes: 35 additions & 0 deletions playgrounds/express/app.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import express from 'express';
import { fileURLToPath } from 'node:url';
import { webdecoy } from '@webdecoy/express';
import { tripwire } from '@webdecoy/node';

// This is a controlled, keyless fixture. Decision details are exposed only so
// visitors can inspect the demo; keep such details server-side in a real app.
function probeRouter(mode) {
const router = express.Router();
router.use(webdecoy({
mode,
rules: [tripwire({ paths: ['/demo-decoy'], includeDefaults: false })],
}));
const reply = (req, res, status) => res.status(status).json({
mode,
path: req.path,
decision: {
conclusion: req.webdecoyDecision?.conclusion,
tripwire: req.webdecoyDecision?.deniedBy('tripwire') ?? false,
},
});
router.get('/', (req, res) => reply(req, res, 200));
router.get('/demo-decoy', (req, res) => reply(req, res, 404));
return router;
}

export function createApp() {
const app = express();
app.disable('x-powered-by');
app.use('/monitor', probeRouter('monitor'));
// A separate fixture, selected explicitly by the playground visitor.
app.use('/enforce', probeRouter('enforce'));
app.use(express.static(fileURLToPath(new URL('./public/', import.meta.url))));
return app;
}
29 changes: 29 additions & 0 deletions playgrounds/express/app.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import { once } from 'node:events';
import { test } from 'node:test';
import { createApp } from './app.mjs';

test('real middleware preserves monitoring responses and blocks only the enforced decoy', async () => {
const server = createApp().listen(0, '127.0.0.1');
await once(server, 'listening');
const base = `http://127.0.0.1:${server.address().port}`;
try {
for (const mode of ['monitor', 'enforce']) {
const normal = await fetch(`${base}/${mode}/`);
assert.equal(normal.status, 200);
assert.deepEqual((await normal.json()).decision, { conclusion: 'ALLOW', tripwire: false });
}
const monitored = await fetch(`${base}/monitor/demo-decoy`);
assert.equal(monitored.status, 404);
assert.deepEqual((await monitored.json()).decision, { conclusion: 'DENY', tripwire: true });
const blocked = await fetch(`${base}/enforce/demo-decoy`);
assert.equal(blocked.status, 403);
assert.equal((await blocked.json()).rule, 'tripwire');
const home = await fetch(base);
assert.equal(home.status, 200);
assert.match(await home.text(), /Enable enforcement for this demo/);
assert.equal((await fetch(`${base}/playground.js`)).status, 200);
} finally {
await new Promise((resolve, reject) => server.close(e => e ? reject(e) : resolve()));
}
});
Loading
Loading