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 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+.
@@ -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"}
@@ -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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"}
@@ -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,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -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
+ }