botshield-express 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +58 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +40 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +207 -0
- package/dist/middleware.js.map +1 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Travelio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# botshield-express
|
|
2
|
+
|
|
3
|
+
Drop-in Express middleware for BotShield.
|
|
4
|
+
|
|
5
|
+
It reads the verdict the gateway already computed, scores the request through the
|
|
6
|
+
engine when there is none, attaches the result to `req.botshield`, and **fails
|
|
7
|
+
open** — an unreachable engine degrades to "no verdict"; it never blocks a
|
|
8
|
+
request and never throws into the request path.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install botshield-express
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`express` is a peer dependency (^4.18) — bring your own.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
const express = require('express');
|
|
22
|
+
const { botShield } = require('botshield-express');
|
|
23
|
+
|
|
24
|
+
const app = express();
|
|
25
|
+
|
|
26
|
+
app.use(
|
|
27
|
+
botShield({
|
|
28
|
+
engineUrl: process.env.ENGINE_URL ?? 'http://engine:4000',
|
|
29
|
+
appName: 'my-app',
|
|
30
|
+
mode: 'shadow', // observes only; never blocks
|
|
31
|
+
protectedRoutes: ['/login*', '/checkout*'],
|
|
32
|
+
logger: console,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
app.get('/login', (req, res) => {
|
|
37
|
+
req.botshield.verdict; // 'allow' | 'notify' | 'challenge' | 'block'
|
|
38
|
+
req.botshield.score; // 0-100
|
|
39
|
+
req.botshield.requestId;
|
|
40
|
+
req.botshield.record('login_attempt', { ok: false }); // business event, fire and forget
|
|
41
|
+
res.sendStatus(200);
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Options
|
|
46
|
+
|
|
47
|
+
| option | default | meaning |
|
|
48
|
+
| ------------------- | -------- | ------------------------------------------------------------------------------ |
|
|
49
|
+
| `engineUrl` | – | Base URL of the BotShield engine (`http://engine:4000` inside the compose net) |
|
|
50
|
+
| `appName` | – | Reported to the engine as the `app` field |
|
|
51
|
+
| `mode` | `shadow` | `shadow` observes only; `enforce` permits block/challenge actions |
|
|
52
|
+
| `protectedRoutes` | `[]` | Patterns the middleware scores; every other request passes through untouched |
|
|
53
|
+
| `fingerprintCookie` | – | Overrides the cookie name the browser sensor sets |
|
|
54
|
+
| `timeoutMs` | – | Bounded time budget for the engine call |
|
|
55
|
+
| `cacheMaxEntries` | `5000` | Entries retained in the in-process verdict cache |
|
|
56
|
+
| `onVerdict` | – | `(context, req) => void` hook |
|
|
57
|
+
| `logger` | – | `{ warn(obj, msg?), error(obj, msg?) }` |
|
|
58
|
+
|
|
59
|
+
## Exports
|
|
60
|
+
|
|
61
|
+
`botShield`, `VerdictCache`, `matchesProtected`, `createEngineClient`, and the
|
|
62
|
+
types `BotShieldConfig`, `BotShieldContext`, `BotShieldMiddleware`, `Verdict`,
|
|
63
|
+
`VerifyRequest`, `EngineClient`, `Action`, `NextFunction`, `Request`,
|
|
64
|
+
`RequestHandler`, `Response`.
|
|
65
|
+
|
|
66
|
+
## Notes
|
|
67
|
+
|
|
68
|
+
- The engine must be reachable. From a container outside the BotShield compose
|
|
69
|
+
project, join its network first (`docker network ls | grep botshield`).
|
|
70
|
+
- Verdicts are cached for the TTL the engine returns, and a verdict supplied by
|
|
71
|
+
the gateway is reused instead of calling the engine again.
|
|
72
|
+
- This package is CommonJS and targets Node 20+.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Verdict, VerifyRequest } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Engine client for the SDK.
|
|
4
|
+
*
|
|
5
|
+
* Three rules, in order of importance:
|
|
6
|
+
* 1. **Fail open.** Any failure — timeout, connection refused, 5xx, malformed
|
|
7
|
+
* JSON — yields `null`, and the caller lets the request through (§3.3).
|
|
8
|
+
* 2. **Bounded latency.** A slow engine must not become a slow application, so
|
|
9
|
+
* every call carries an AbortController deadline.
|
|
10
|
+
* 3. **Never throw.** The request path does not tolerate exceptions from here.
|
|
11
|
+
*/
|
|
12
|
+
export interface EngineClientOptions {
|
|
13
|
+
engineUrl: string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const DEFAULT_TIMEOUT_MS = 1000;
|
|
17
|
+
export interface EngineClient {
|
|
18
|
+
verify(input: VerifyRequest): Promise<Verdict | null>;
|
|
19
|
+
recordEvent(input: {
|
|
20
|
+
app: string;
|
|
21
|
+
event: string;
|
|
22
|
+
occurredAt: string;
|
|
23
|
+
subject: {
|
|
24
|
+
ip: string;
|
|
25
|
+
userId?: string;
|
|
26
|
+
fingerprint?: string;
|
|
27
|
+
};
|
|
28
|
+
attributes: Record<string, unknown>;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare function createEngineClient(options: EngineClientOptions): EngineClient;
|
|
32
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEtD;;;;;;;;;GASG;AAEH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACtD,WAAW,CAAC,KAAK,EAAE;QACjB,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC/D,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AA4CD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAiB7E"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
4
|
+
exports.createEngineClient = createEngineClient;
|
|
5
|
+
exports.DEFAULT_TIMEOUT_MS = 1_000;
|
|
6
|
+
function isVerdict(value) {
|
|
7
|
+
if (typeof value !== 'object' || value === null)
|
|
8
|
+
return false;
|
|
9
|
+
const candidate = value;
|
|
10
|
+
return (typeof candidate.action === 'string' &&
|
|
11
|
+
typeof candidate.score === 'number' &&
|
|
12
|
+
typeof candidate.verdict === 'string');
|
|
13
|
+
}
|
|
14
|
+
async function postJson(url, body, timeoutMs) {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify(body),
|
|
22
|
+
signal: controller.signal,
|
|
23
|
+
});
|
|
24
|
+
const text = await response.text();
|
|
25
|
+
let parsed = null;
|
|
26
|
+
try {
|
|
27
|
+
parsed = text === '' ? null : JSON.parse(text);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
parsed = null;
|
|
31
|
+
}
|
|
32
|
+
return { ok: response.ok, status: response.status, body: parsed };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function createEngineClient(options) {
|
|
42
|
+
const timeoutMs = options.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS;
|
|
43
|
+
const base = options.engineUrl.replace(/\/+$/, '');
|
|
44
|
+
return {
|
|
45
|
+
async verify(input) {
|
|
46
|
+
const result = await postJson(`${base}/verify`, input, timeoutMs);
|
|
47
|
+
if (!result || !result.ok)
|
|
48
|
+
return null;
|
|
49
|
+
return isVerdict(result.body) ? result.body : null;
|
|
50
|
+
},
|
|
51
|
+
async recordEvent(input) {
|
|
52
|
+
// Business-event reporting is explicitly fire-and-forget. The /events
|
|
53
|
+
// endpoint lands in Phase 3; until then this is a tolerated 404.
|
|
54
|
+
await postJson(`${base}/events`, input, timeoutMs);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;AAyEA,gDAiBC;AAxEY,QAAA,kBAAkB,GAAG,KAAK,CAAC;AAaxC,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,SAAS,GAAG,KAAyB,CAAC;IAC5C,OAAO,CACL,OAAO,SAAS,CAAC,MAAM,KAAK,QAAQ;QACpC,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ;QACnC,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CACtC,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,QAAQ,CACrB,GAAW,EACX,IAAa,EACb,SAAiB;IAEjB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IAE9D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAC1B,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,MAAM,GAAY,IAAI,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QAED,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,SAAgB,kBAAkB,CAAC,OAA4B;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,0BAAkB,CAAC;IAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAEnD,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,KAAoB;YAC/B,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,GAAG,IAAI,SAAS,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;YAClE,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC;YACvC,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QACrD,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,KAAK;YACrB,sEAAsE;YACtE,iEAAiE;YACjE,MAAM,QAAQ,CAAC,GAAG,IAAI,SAAS,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;QACrD,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { botShield, VerdictCache, matchesProtected } from './middleware';
|
|
2
|
+
export { createEngineClient } from './client';
|
|
3
|
+
export type { Action, BotShieldConfig, BotShieldContext, BotShieldMiddleware, Verdict, VerifyRequest, } from './types';
|
|
4
|
+
export type { EngineClient } from './client';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,YAAY,EACV,MAAM,EACN,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,OAAO,EACP,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createEngineClient = exports.matchesProtected = exports.VerdictCache = exports.botShield = void 0;
|
|
4
|
+
var middleware_1 = require("./middleware");
|
|
5
|
+
Object.defineProperty(exports, "botShield", { enumerable: true, get: function () { return middleware_1.botShield; } });
|
|
6
|
+
Object.defineProperty(exports, "VerdictCache", { enumerable: true, get: function () { return middleware_1.VerdictCache; } });
|
|
7
|
+
Object.defineProperty(exports, "matchesProtected", { enumerable: true, get: function () { return middleware_1.matchesProtected; } });
|
|
8
|
+
var client_1 = require("./client");
|
|
9
|
+
Object.defineProperty(exports, "createEngineClient", { enumerable: true, get: function () { return client_1.createEngineClient; } });
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2CAAyE;AAAhE,uGAAA,SAAS,OAAA;AAAE,0GAAA,YAAY,OAAA;AAAE,8GAAA,gBAAgB,OAAA;AAClD,mCAA8C;AAArC,4GAAA,kBAAkB,OAAA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createEngineClient, type EngineClient } from './client';
|
|
2
|
+
import type { BotShieldConfig, BotShieldMiddleware, Verdict } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* botshield-express middleware (§15).
|
|
5
|
+
*
|
|
6
|
+
* Requirements this file exists to satisfy:
|
|
7
|
+
* - fail open when the engine is unreachable
|
|
8
|
+
* - never throw into the request path
|
|
9
|
+
* - cache verdicts locally for the TTL the engine returned
|
|
10
|
+
* - skip the engine call entirely when the gateway already supplied a verdict
|
|
11
|
+
*
|
|
12
|
+
* Every request-scoped value lives in a closure created per request. There is
|
|
13
|
+
* deliberately no shared mutable state: a module-level "current request" would
|
|
14
|
+
* leak between concurrent requests, which is the classic way an observability
|
|
15
|
+
* middleware becomes a security hole.
|
|
16
|
+
*/
|
|
17
|
+
export interface VerdictCacheEntry {
|
|
18
|
+
verdict: Verdict;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
}
|
|
21
|
+
/** Small bounded cache; Map insertion order doubles as eviction order. */
|
|
22
|
+
export declare class VerdictCache {
|
|
23
|
+
private readonly maxEntries;
|
|
24
|
+
private readonly entries;
|
|
25
|
+
constructor(maxEntries?: number);
|
|
26
|
+
get(key: string, now?: number): Verdict | null;
|
|
27
|
+
set(key: string, verdict: Verdict, now?: number): void;
|
|
28
|
+
get size(): number;
|
|
29
|
+
}
|
|
30
|
+
export interface RequestScope {
|
|
31
|
+
ip: string;
|
|
32
|
+
fingerprint?: string | undefined;
|
|
33
|
+
sessionId?: string | undefined;
|
|
34
|
+
userId?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
export declare function matchesProtected(path: string, patterns: readonly string[]): boolean;
|
|
37
|
+
export declare function botShield(config: BotShieldConfig): BotShieldMiddleware;
|
|
38
|
+
export { createEngineClient, VerdictCache as VerdictCacheImpl };
|
|
39
|
+
export type { EngineClient };
|
|
40
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AACjE,OAAO,KAAK,EAEV,eAAe,EAEf,mBAAmB,EAKnB,OAAO,EACR,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,0EAA0E;AAC1E,qBAAa,YAAY;IAGX,OAAO,CAAC,QAAQ,CAAC,UAAU;IAFvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwC;gBAEnC,UAAU,SAAQ;IAE/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,OAAO,GAAG,IAAI;IAYlD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,SAAa,GAAG,IAAI;IAU1D,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAsCD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAKnF;AAQD,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,mBAAmB,CAqItE;AAED,OAAO,EAAE,kBAAkB,EAAE,YAAY,IAAI,gBAAgB,EAAE,CAAC;AAChE,YAAY,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VerdictCacheImpl = exports.createEngineClient = exports.VerdictCache = void 0;
|
|
4
|
+
exports.matchesProtected = matchesProtected;
|
|
5
|
+
exports.botShield = botShield;
|
|
6
|
+
const client_1 = require("./client");
|
|
7
|
+
Object.defineProperty(exports, "createEngineClient", { enumerable: true, get: function () { return client_1.createEngineClient; } });
|
|
8
|
+
/** Small bounded cache; Map insertion order doubles as eviction order. */
|
|
9
|
+
class VerdictCache {
|
|
10
|
+
maxEntries;
|
|
11
|
+
entries = new Map();
|
|
12
|
+
constructor(maxEntries = 5_000) {
|
|
13
|
+
this.maxEntries = maxEntries;
|
|
14
|
+
}
|
|
15
|
+
get(key, now = Date.now()) {
|
|
16
|
+
const entry = this.entries.get(key);
|
|
17
|
+
if (!entry)
|
|
18
|
+
return null;
|
|
19
|
+
if (entry.expiresAt <= now) {
|
|
20
|
+
this.entries.delete(key);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
this.entries.delete(key);
|
|
24
|
+
this.entries.set(key, entry);
|
|
25
|
+
return entry.verdict;
|
|
26
|
+
}
|
|
27
|
+
set(key, verdict, now = Date.now()) {
|
|
28
|
+
this.entries.set(key, { verdict, expiresAt: now + Math.max(0, verdict.ttl) * 1000 });
|
|
29
|
+
while (this.entries.size > this.maxEntries) {
|
|
30
|
+
const oldest = this.entries.keys().next();
|
|
31
|
+
if (oldest.done)
|
|
32
|
+
break;
|
|
33
|
+
this.entries.delete(oldest.value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
get size() {
|
|
37
|
+
return this.entries.size;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.VerdictCache = VerdictCache;
|
|
41
|
+
exports.VerdictCacheImpl = VerdictCache;
|
|
42
|
+
function parseCookies(header) {
|
|
43
|
+
const cookies = {};
|
|
44
|
+
if (!header)
|
|
45
|
+
return cookies;
|
|
46
|
+
for (const part of header.split(';')) {
|
|
47
|
+
const index = part.indexOf('=');
|
|
48
|
+
if (index === -1)
|
|
49
|
+
continue;
|
|
50
|
+
const name = part.slice(0, index).trim();
|
|
51
|
+
if (name === '')
|
|
52
|
+
continue;
|
|
53
|
+
const raw = part.slice(index + 1).trim();
|
|
54
|
+
try {
|
|
55
|
+
cookies[name] = decodeURIComponent(raw);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
cookies[name] = raw;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return cookies;
|
|
62
|
+
}
|
|
63
|
+
function headerValue(headers, name) {
|
|
64
|
+
const value = headers[name];
|
|
65
|
+
if (typeof value === 'string')
|
|
66
|
+
return value;
|
|
67
|
+
if (Array.isArray(value) && typeof value[0] === 'string')
|
|
68
|
+
return value[0];
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
function toHeaderRecord(headers) {
|
|
72
|
+
const result = {};
|
|
73
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
74
|
+
if (typeof value === 'string')
|
|
75
|
+
result[name] = value;
|
|
76
|
+
else if (Array.isArray(value))
|
|
77
|
+
result[name] = value.join(', ');
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function matchesProtected(path, patterns) {
|
|
82
|
+
return patterns.some((pattern) => {
|
|
83
|
+
if (pattern.endsWith('*'))
|
|
84
|
+
return path.startsWith(pattern.slice(0, -1));
|
|
85
|
+
return path === pattern;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function actionFromHeader(value) {
|
|
89
|
+
return value === 'allow' || value === 'notify' || value === 'challenge' || value === 'block'
|
|
90
|
+
? value
|
|
91
|
+
: null;
|
|
92
|
+
}
|
|
93
|
+
function botShield(config) {
|
|
94
|
+
const mode = config.mode ?? 'shadow';
|
|
95
|
+
const protectedRoutes = config.protectedRoutes ?? [];
|
|
96
|
+
const fingerprintCookie = config.fingerprintCookie ?? 'bs_fp';
|
|
97
|
+
const cache = new VerdictCache(config.cacheMaxEntries ?? 5_000);
|
|
98
|
+
const client = (0, client_1.createEngineClient)({
|
|
99
|
+
engineUrl: config.engineUrl,
|
|
100
|
+
timeoutMs: config.timeoutMs,
|
|
101
|
+
});
|
|
102
|
+
function buildContext(verdict, score, requestId, cached, scope) {
|
|
103
|
+
return {
|
|
104
|
+
verdict,
|
|
105
|
+
score,
|
|
106
|
+
requestId,
|
|
107
|
+
cached,
|
|
108
|
+
record(event, attributes) {
|
|
109
|
+
// Fire-and-forget (§15): not awaited, cannot fail the request.
|
|
110
|
+
void client
|
|
111
|
+
.recordEvent({
|
|
112
|
+
app: config.appName,
|
|
113
|
+
event,
|
|
114
|
+
occurredAt: new Date().toISOString(),
|
|
115
|
+
subject: {
|
|
116
|
+
ip: scope.ip,
|
|
117
|
+
...(scope.userId ? { userId: scope.userId } : {}),
|
|
118
|
+
...(scope.fingerprint ? { fingerprint: scope.fingerprint } : {}),
|
|
119
|
+
},
|
|
120
|
+
attributes,
|
|
121
|
+
})
|
|
122
|
+
.catch(() => undefined);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const handler = async (req, res, next) => {
|
|
127
|
+
const headers = toHeaderRecord(req.headers);
|
|
128
|
+
const cookies = parseCookies(headerValue(headers, 'cookie'));
|
|
129
|
+
const scope = {
|
|
130
|
+
ip: headerValue(headers, 'x-real-ip') ?? req.socket?.remoteAddress ?? '0.0.0.0',
|
|
131
|
+
fingerprint: cookies[fingerprintCookie],
|
|
132
|
+
sessionId: cookies.bs_sid,
|
|
133
|
+
userId: req.user?.id,
|
|
134
|
+
};
|
|
135
|
+
/** Attaches the context and lets the request proceed. */
|
|
136
|
+
const pass = (context) => {
|
|
137
|
+
req.botshield = context;
|
|
138
|
+
config.onVerdict?.(context, req);
|
|
139
|
+
next();
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
// The gateway already decided — trust it and skip the engine (§13.1, §15).
|
|
143
|
+
const gatewayVerdict = actionFromHeader(headerValue(headers, 'x-bot-verdict'));
|
|
144
|
+
if (gatewayVerdict !== null) {
|
|
145
|
+
pass(buildContext(gatewayVerdict, Number(headerValue(headers, 'x-bot-score') ?? 0), headerValue(headers, 'x-request-id') ?? 'gateway', true, scope));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const cacheKey = `${scope.fingerprint ?? 'no-fp'}|${scope.ip}`;
|
|
149
|
+
const cached = scope.fingerprint ? cache.get(cacheKey) : null;
|
|
150
|
+
if (cached) {
|
|
151
|
+
pass(buildContext(cached.action, cached.score, cached.requestId, true, scope));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const pageLoadTs = cookies.bs_load ? Number(cookies.bs_load) : undefined;
|
|
155
|
+
const submitTs = cookies.bs_submit ? Number(cookies.bs_submit) : undefined;
|
|
156
|
+
const ja4 = headerValue(headers, 'x-ja4-hash');
|
|
157
|
+
const verdict = await client.verify({
|
|
158
|
+
app: config.appName,
|
|
159
|
+
method: req.method,
|
|
160
|
+
path: req.originalUrl ?? req.url,
|
|
161
|
+
ip: scope.ip,
|
|
162
|
+
...(scope.userId ? { userId: scope.userId } : {}),
|
|
163
|
+
...(scope.fingerprint ? { fingerprint: scope.fingerprint } : {}),
|
|
164
|
+
...(scope.sessionId ? { sessionId: scope.sessionId } : {}),
|
|
165
|
+
...(ja4 ? { ja4 } : {}),
|
|
166
|
+
headers,
|
|
167
|
+
...(pageLoadTs !== undefined && submitTs !== undefined
|
|
168
|
+
? { timing: { pageLoadTs, submitTs } }
|
|
169
|
+
: {}),
|
|
170
|
+
});
|
|
171
|
+
if (!verdict) {
|
|
172
|
+
// Fail open: engine down, too slow, or nonsense (§3.3, §15).
|
|
173
|
+
pass(buildContext('allow', 0, 'unavailable', false, scope));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (scope.fingerprint)
|
|
177
|
+
cache.set(cacheKey, verdict);
|
|
178
|
+
const context = buildContext(verdict.action, verdict.score, verdict.requestId, verdict.cached, scope);
|
|
179
|
+
if (mode === 'enforce' &&
|
|
180
|
+
context.verdict === 'block' &&
|
|
181
|
+
matchesProtected(req.path, protectedRoutes)) {
|
|
182
|
+
req.botshield = context;
|
|
183
|
+
config.onVerdict?.(context, req);
|
|
184
|
+
res.status(403).json({ error: 'forbidden', requestId: context.requestId });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// `challenge` is intentionally not enforced here: §19 schedules the
|
|
188
|
+
// proof-of-work endpoint for a later phase, and a redirect to a route
|
|
189
|
+
// that does not exist would be worse than observing.
|
|
190
|
+
pass(context);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
// The request path must never see an exception from here (§15).
|
|
194
|
+
try {
|
|
195
|
+
config.logger?.error({ err: error }, 'botshield middleware failed — failing open');
|
|
196
|
+
req.botshield = buildContext('allow', 0, 'error', false, scope);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
/* nothing more we can safely do */
|
|
200
|
+
}
|
|
201
|
+
next();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const wrapped = Object.assign(handler, { client });
|
|
205
|
+
return wrapped;
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
2
|
+
/** Verdict actions, mirroring §9.2. */
|
|
3
|
+
export type Action = 'allow' | 'notify' | 'challenge' | 'block';
|
|
4
|
+
export interface Verdict {
|
|
5
|
+
verdict: Action;
|
|
6
|
+
action: Action;
|
|
7
|
+
score: number;
|
|
8
|
+
breakdown: Record<string, number>;
|
|
9
|
+
rulesTripped: string[];
|
|
10
|
+
requestId: string;
|
|
11
|
+
ttl: number;
|
|
12
|
+
cached: boolean;
|
|
13
|
+
shortCircuited: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** What the middleware attaches to each request. */
|
|
16
|
+
export interface BotShieldContext {
|
|
17
|
+
verdict: Action;
|
|
18
|
+
score: number;
|
|
19
|
+
requestId: string;
|
|
20
|
+
/** True when the verdict came from a cache (gateway header or local cache). */
|
|
21
|
+
cached: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Reports a business event (§6.1). Fire-and-forget: it never throws and never
|
|
24
|
+
* delays the response. The /events endpoint this targets arrives in Phase 3.
|
|
25
|
+
*/
|
|
26
|
+
record(event: string, attributes: Record<string, unknown>): void;
|
|
27
|
+
}
|
|
28
|
+
export interface BotShieldConfig {
|
|
29
|
+
engineUrl: string;
|
|
30
|
+
appName: string;
|
|
31
|
+
/** `shadow` observes only; `enforce` permits block/challenge. */
|
|
32
|
+
mode?: 'shadow' | 'enforce';
|
|
33
|
+
protectedRoutes?: string[];
|
|
34
|
+
/** Overrides the sensor cookie name the SDK reads the fingerprint from. */
|
|
35
|
+
fingerprintCookie?: string;
|
|
36
|
+
/** Bounded time budget for the engine call, ms. */
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
/** Entries retained in the in-process verdict cache. */
|
|
39
|
+
cacheMaxEntries?: number;
|
|
40
|
+
onVerdict?: (context: BotShieldContext, req: Request) => void;
|
|
41
|
+
logger?: {
|
|
42
|
+
warn(obj: unknown, msg?: string): void;
|
|
43
|
+
error(obj: unknown, msg?: string): void;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
declare module 'express-serve-static-core' {
|
|
47
|
+
interface Request {
|
|
48
|
+
botshield?: BotShieldContext;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export type BotShieldMiddleware = RequestHandler & {
|
|
52
|
+
/** Exposed for tests. */
|
|
53
|
+
readonly client: {
|
|
54
|
+
verify(input: VerifyRequest): Promise<Verdict | null>;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export interface VerifyRequest {
|
|
58
|
+
app: string;
|
|
59
|
+
method: string;
|
|
60
|
+
path: string;
|
|
61
|
+
ip: string;
|
|
62
|
+
/** Forwarded so userId-scoped exemptions can be enforced server-side. */
|
|
63
|
+
userId?: string;
|
|
64
|
+
fingerprint?: string;
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
ja4?: string;
|
|
67
|
+
headers: Record<string, string>;
|
|
68
|
+
timing?: {
|
|
69
|
+
pageLoadTs?: number;
|
|
70
|
+
submitTs?: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export type { NextFunction, Request, RequestHandler, Response };
|
|
74
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/E,uCAAuC;AACvC,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAEhE,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,oDAAoD;AACpD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,MAAM,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAClE;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9D,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CAC9F;AAED,OAAO,QAAQ,2BAA2B,CAAC;IACzC,UAAU,OAAO;QACf,SAAS,CAAC,EAAE,gBAAgB,CAAC;KAC9B;CACF;AAED,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG;IACjD,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE;QAAE,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;KAAE,CAAC;CAC5E,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAED,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "botshield-express",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop-in Express middleware for BotShield — reads gateway verdicts, scores via the engine, fails open",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.build.json",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"lint": "eslint src",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"express": "^4.18.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/express": "4.17.25",
|
|
34
|
+
"@types/node": "20.19.43",
|
|
35
|
+
"@types/supertest": "7.2.1",
|
|
36
|
+
"express": "4.22.3",
|
|
37
|
+
"supertest": "7.2.2",
|
|
38
|
+
"vitest": "2.1.9"
|
|
39
|
+
}
|
|
40
|
+
}
|