devn-bot-detector-server 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 +55 -0
- package/dist/index.cjs +912 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +258 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +877 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var devnBotDetectorCore = require('devn-bot-detector-core');
|
|
4
|
+
var crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
// src/BotDetectionServer.ts
|
|
7
|
+
function secureId(prefix, bytes = 16) {
|
|
8
|
+
return `${prefix}_${crypto.randomBytes(bytes).toString("hex")}`;
|
|
9
|
+
}
|
|
10
|
+
var MemorySiteResolver = class {
|
|
11
|
+
constructor(entries = []) {
|
|
12
|
+
this.sites = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
this.sites.set(entry.siteKey, entry.site);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
set(siteKey, site) {
|
|
18
|
+
this.sites.set(siteKey, site);
|
|
19
|
+
}
|
|
20
|
+
async resolveSite(siteKey) {
|
|
21
|
+
return this.sites.get(siteKey) ?? null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var MemorySessionStore = class {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
27
|
+
}
|
|
28
|
+
async create(session) {
|
|
29
|
+
this.sessions.set(session.sessionId, {
|
|
30
|
+
...session,
|
|
31
|
+
usedNonces: [...session.usedNonces]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async get(sessionId) {
|
|
35
|
+
const s = this.sessions.get(sessionId);
|
|
36
|
+
if (!s) return null;
|
|
37
|
+
return {
|
|
38
|
+
...s,
|
|
39
|
+
usedNonces: [...s.usedNonces]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async update(session) {
|
|
43
|
+
this.sessions.set(session.sessionId, {
|
|
44
|
+
...session,
|
|
45
|
+
usedNonces: [...session.usedNonces]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async invalidate(sessionId) {
|
|
49
|
+
this.sessions.delete(sessionId);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var MemoryTokenRevocationStore = class {
|
|
53
|
+
constructor() {
|
|
54
|
+
this.revoked = /* @__PURE__ */ new Map();
|
|
55
|
+
}
|
|
56
|
+
isRevoked(tokenId) {
|
|
57
|
+
const expiresAt = this.revoked.get(tokenId);
|
|
58
|
+
if (!expiresAt) return false;
|
|
59
|
+
if (Date.now() >= expiresAt) {
|
|
60
|
+
this.revoked.delete(tokenId);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
revoke(tokenId, ttlMs) {
|
|
66
|
+
this.revoked.set(tokenId, Date.now() + Math.max(1e3, ttlMs));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/rate-limit.ts
|
|
71
|
+
var MemoryRateLimiter = class {
|
|
72
|
+
constructor() {
|
|
73
|
+
this.buckets = /* @__PURE__ */ new Map();
|
|
74
|
+
}
|
|
75
|
+
check(key, limit, windowMs) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
let bucket = this.buckets.get(key);
|
|
78
|
+
if (!bucket || now >= bucket.resetAt) {
|
|
79
|
+
bucket = { count: 0, resetAt: now + windowMs };
|
|
80
|
+
this.buckets.set(key, bucket);
|
|
81
|
+
}
|
|
82
|
+
if (bucket.count >= limit) {
|
|
83
|
+
return {
|
|
84
|
+
allowed: false,
|
|
85
|
+
remaining: 0,
|
|
86
|
+
retryAfter: Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3))
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
bucket.count += 1;
|
|
90
|
+
return {
|
|
91
|
+
allowed: true,
|
|
92
|
+
remaining: Math.max(0, limit - bucket.count)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Best-effort cleanup for long-running processes. */
|
|
96
|
+
prune() {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
for (const [key, bucket] of this.buckets) {
|
|
99
|
+
if (now >= bucket.resetAt) this.buckets.delete(key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var AutomationDetector = (input) => {
|
|
104
|
+
const reasons = [];
|
|
105
|
+
const browser = input.telemetry.browser;
|
|
106
|
+
if (!browser) return reasons;
|
|
107
|
+
if (browser.webdriver) {
|
|
108
|
+
reasons.push({ code: "WEBDRIVER", weight: 25, detail: "navigator.webdriver true" });
|
|
109
|
+
}
|
|
110
|
+
const ua = browser.userAgent.toLowerCase();
|
|
111
|
+
if (ua.includes("headless") || ua.includes("phantomjs")) {
|
|
112
|
+
reasons.push({ code: "HEADLESS_UA", weight: 20 });
|
|
113
|
+
}
|
|
114
|
+
return reasons;
|
|
115
|
+
};
|
|
116
|
+
var HeadlessAdvancedDetector = (input) => {
|
|
117
|
+
const reasons = [];
|
|
118
|
+
const browser = input.telemetry.browser;
|
|
119
|
+
if (!browser) return reasons;
|
|
120
|
+
const renderer = (browser.webglRenderer || "").toLowerCase();
|
|
121
|
+
if (renderer.includes("swiftshader") || renderer.includes("llvmpipe") || renderer.includes("softpipe") || renderer.includes("mesa offscreen")) {
|
|
122
|
+
reasons.push({
|
|
123
|
+
code: "SOFTWARE_WEBGL",
|
|
124
|
+
weight: 22,
|
|
125
|
+
detail: "Software WebGL renderer detected (virtualized/headless)"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (browser.userAgent.includes("Chrome") && browser.hasChrome === false) {
|
|
129
|
+
reasons.push({
|
|
130
|
+
code: "MISSING_CHROME_OBJ",
|
|
131
|
+
weight: 18,
|
|
132
|
+
detail: "UA reports Chrome but window.chrome is absent"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const ua = browser.userAgent.toLowerCase();
|
|
136
|
+
const isDesktop = !ua.includes("mobile") && !ua.includes("android") && !ua.includes("iphone") && !ua.includes("ipad") && !ua.includes("jsdom");
|
|
137
|
+
if (isDesktop && browser.pluginsCount === 0) {
|
|
138
|
+
reasons.push({ code: "ZERO_DESKTOP_PLUGINS", weight: 12 });
|
|
139
|
+
}
|
|
140
|
+
return reasons;
|
|
141
|
+
};
|
|
142
|
+
var EntropyDetector = (input) => {
|
|
143
|
+
const reasons = [];
|
|
144
|
+
const pointer = input.telemetry.pointer;
|
|
145
|
+
if (pointer && pointer.eventCount >= 25 && pointer.movementDistance > 350 && pointer.directionChanges === 0) {
|
|
146
|
+
reasons.push({
|
|
147
|
+
code: "LINEAR_POINTER_PATH",
|
|
148
|
+
weight: 20,
|
|
149
|
+
detail: "Unnatural linear pointer trajectory without direction variance"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return reasons;
|
|
153
|
+
};
|
|
154
|
+
var ActionRiskDetector = (input) => {
|
|
155
|
+
const reasons = [];
|
|
156
|
+
if (!input.action) return reasons;
|
|
157
|
+
const action = input.action.toLowerCase();
|
|
158
|
+
const sensitivePatterns = ["login", "checkout", "pay", "transfer", "signup", "reset_password"];
|
|
159
|
+
const isSensitive = sensitivePatterns.some((p) => action.includes(p));
|
|
160
|
+
if (isSensitive) {
|
|
161
|
+
const interaction = input.telemetry.interaction;
|
|
162
|
+
if (interaction && interaction.interactionCount === 0) {
|
|
163
|
+
reasons.push({
|
|
164
|
+
code: "SENSITIVE_ACTION_NO_INTERACTION",
|
|
165
|
+
weight: 25,
|
|
166
|
+
detail: `Sensitive action "${input.action}" attempted with 0 prior interactions`
|
|
167
|
+
});
|
|
168
|
+
} else if (interaction && interaction.timeToFirstInteraction != null && interaction.timeToFirstInteraction < 150 && interaction.interactionCount > 0) {
|
|
169
|
+
reasons.push({
|
|
170
|
+
code: "SENSITIVE_ACTION_SUB_HUMAN_SPEED",
|
|
171
|
+
weight: 20,
|
|
172
|
+
detail: `Sensitive action "${input.action}" executed under 150ms`
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return reasons;
|
|
177
|
+
};
|
|
178
|
+
var VelocityDetector = (input) => {
|
|
179
|
+
const reasons = [];
|
|
180
|
+
if (input.sequence > 40 && input.sessionAgeMs < 1e4) {
|
|
181
|
+
reasons.push({ code: "HIGH_SEQUENCE_VELOCITY", weight: 30 });
|
|
182
|
+
}
|
|
183
|
+
const pointer = input.telemetry.pointer;
|
|
184
|
+
if (pointer && pointer.eventCount > 800 && pointer.activeDuration < 2e3) {
|
|
185
|
+
reasons.push({ code: "POINTER_VELOCITY", weight: 20 });
|
|
186
|
+
}
|
|
187
|
+
return reasons;
|
|
188
|
+
};
|
|
189
|
+
var TimingDetector = (input) => {
|
|
190
|
+
const reasons = [];
|
|
191
|
+
const kb = input.telemetry.keyboard;
|
|
192
|
+
if (kb && kb.keydownCount >= 12 && kb.timingVariance != null && kb.timingVariance < 1) {
|
|
193
|
+
reasons.push({ code: "KEYBOARD_TOO_UNIFORM", weight: 18 });
|
|
194
|
+
}
|
|
195
|
+
if (kb && kb.averageInterval != null && kb.averageInterval < 15 && kb.keydownCount >= 10) {
|
|
196
|
+
reasons.push({ code: "KEYBOARD_TOO_FAST", weight: 15 });
|
|
197
|
+
}
|
|
198
|
+
const interaction = input.telemetry.interaction;
|
|
199
|
+
if (interaction && interaction.timeToFirstInteraction != null && interaction.timeToFirstInteraction < 20 && interaction.interactionCount > 0) {
|
|
200
|
+
reasons.push({ code: "IMPOSSIBLE_TTFI", weight: 12 });
|
|
201
|
+
}
|
|
202
|
+
return reasons;
|
|
203
|
+
};
|
|
204
|
+
var InteractionDetector = (input) => {
|
|
205
|
+
const reasons = [];
|
|
206
|
+
const interaction = input.telemetry.interaction;
|
|
207
|
+
if (!interaction) return reasons;
|
|
208
|
+
if (interaction.interactionCount === 0) {
|
|
209
|
+
reasons.push({ code: "ZERO_INTERACTION", weight: 10 });
|
|
210
|
+
}
|
|
211
|
+
if (interaction.timeToFirstInteraction != null && interaction.timeToFirstInteraction > 200 && interaction.timeToFirstInteraction < 12e4) {
|
|
212
|
+
reasons.push({ code: "NATURAL_TTFI", weight: -8 });
|
|
213
|
+
}
|
|
214
|
+
if (interaction.interactionCount >= 3) {
|
|
215
|
+
reasons.push({ code: "MIXED_INTERACTION", weight: -6 });
|
|
216
|
+
}
|
|
217
|
+
return reasons;
|
|
218
|
+
};
|
|
219
|
+
var BrowserConsistencyDetector = (input) => {
|
|
220
|
+
const reasons = [];
|
|
221
|
+
const browser = input.telemetry.browser;
|
|
222
|
+
if (!browser) return reasons;
|
|
223
|
+
if (browser.languagesLength === 0) {
|
|
224
|
+
reasons.push({ code: "NO_LANGUAGES", weight: 8 });
|
|
225
|
+
}
|
|
226
|
+
if (browser.screenWidth <= 0 || browser.screenHeight <= 0) {
|
|
227
|
+
reasons.push({ code: "INVALID_SCREEN", weight: 10 });
|
|
228
|
+
}
|
|
229
|
+
if (input.request.userAgent && browser.userAgent) {
|
|
230
|
+
const reqUa = input.request.userAgent.slice(0, 64);
|
|
231
|
+
const telUa = browser.userAgent.slice(0, 64);
|
|
232
|
+
if (reqUa && telUa && reqUa !== telUa) {
|
|
233
|
+
reasons.push({ code: "UA_MISMATCH", weight: 15 });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return reasons;
|
|
237
|
+
};
|
|
238
|
+
var SessionDetector = (input) => {
|
|
239
|
+
const reasons = [];
|
|
240
|
+
if (input.sessionAgeMs < 50 && input.sequence > 1) {
|
|
241
|
+
reasons.push({ code: "SESSION_TOO_YOUNG", weight: 12 });
|
|
242
|
+
}
|
|
243
|
+
const pointer = input.telemetry.pointer;
|
|
244
|
+
if (pointer && pointer.eventCount >= 20 && pointer.directionChanges === 0) {
|
|
245
|
+
reasons.push({ code: "POINTER_NO_DIRECTION_CHANGE", weight: 10 });
|
|
246
|
+
}
|
|
247
|
+
if (pointer && pointer.movementDistance > 0 && pointer.directionChanges > 5) {
|
|
248
|
+
reasons.push({ code: "NATURAL_POINTER", weight: -8 });
|
|
249
|
+
}
|
|
250
|
+
const scroll = input.telemetry.scroll;
|
|
251
|
+
if (scroll && scroll.scrollCount > 0) {
|
|
252
|
+
reasons.push({ code: "HAS_SCROLL", weight: -4 });
|
|
253
|
+
}
|
|
254
|
+
const focus = input.telemetry.focus;
|
|
255
|
+
if (focus && (focus.focusCount > 0 || focus.blurCount > 0)) {
|
|
256
|
+
reasons.push({ code: "HAS_FOCUS_CHANGES", weight: -3 });
|
|
257
|
+
}
|
|
258
|
+
return reasons;
|
|
259
|
+
};
|
|
260
|
+
var DEFAULT_DETECTORS = [
|
|
261
|
+
AutomationDetector,
|
|
262
|
+
HeadlessAdvancedDetector,
|
|
263
|
+
EntropyDetector,
|
|
264
|
+
ActionRiskDetector,
|
|
265
|
+
VelocityDetector,
|
|
266
|
+
TimingDetector,
|
|
267
|
+
InteractionDetector,
|
|
268
|
+
BrowserConsistencyDetector,
|
|
269
|
+
SessionDetector
|
|
270
|
+
];
|
|
271
|
+
var RiskEngine = class {
|
|
272
|
+
constructor(options) {
|
|
273
|
+
if (Array.isArray(options)) {
|
|
274
|
+
this.detectors = options;
|
|
275
|
+
this.weights = {};
|
|
276
|
+
} else {
|
|
277
|
+
this.detectors = options?.detectors ?? DEFAULT_DETECTORS;
|
|
278
|
+
this.weights = options?.detectorWeights ?? {};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
evaluate(input) {
|
|
282
|
+
const internalReasons = [];
|
|
283
|
+
for (const detector of this.detectors) {
|
|
284
|
+
for (const reason of detector(input)) {
|
|
285
|
+
const configuredWeight = this.weights[reason.code];
|
|
286
|
+
const weight = configuredWeight !== void 0 ? configuredWeight : reason.weight;
|
|
287
|
+
if (weight !== 0) {
|
|
288
|
+
internalReasons.push({ ...reason, weight });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
let riskScore = 20;
|
|
293
|
+
for (const reason of internalReasons) {
|
|
294
|
+
riskScore += reason.weight;
|
|
295
|
+
}
|
|
296
|
+
riskScore = devnBotDetectorCore.clamp(riskScore, 0, 100);
|
|
297
|
+
const score = devnBotDetectorCore.clamp(100 - riskScore, 0, 100);
|
|
298
|
+
const confidence = devnBotDetectorCore.clamp(0.4 + Math.abs(score - 50) / 100, 0.4, 0.95);
|
|
299
|
+
const riskLevel = score >= 70 ? "low" : score >= 40 ? "medium" : "high";
|
|
300
|
+
return {
|
|
301
|
+
score,
|
|
302
|
+
confidence: Number(confidence.toFixed(2)),
|
|
303
|
+
riskLevel,
|
|
304
|
+
internalReasons
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
function b64url(input) {
|
|
309
|
+
const buf = typeof input === "string" ? Buffer.from(input, "utf8") : input;
|
|
310
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
311
|
+
}
|
|
312
|
+
function fromB64url(input) {
|
|
313
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
314
|
+
const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
|
|
315
|
+
return Buffer.from(padded + pad, "base64");
|
|
316
|
+
}
|
|
317
|
+
var TokenManager = class {
|
|
318
|
+
constructor(secret) {
|
|
319
|
+
this.secret = secret;
|
|
320
|
+
if (!secret || secret.length < 16) {
|
|
321
|
+
throw new Error("TokenManager requires a secret of at least 16 characters");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
createRiskToken(payload) {
|
|
325
|
+
const body = b64url(JSON.stringify(payload));
|
|
326
|
+
const sig = b64url(crypto.createHmac("sha256", this.secret).update(body).digest());
|
|
327
|
+
return `rt_${body}.${sig}`;
|
|
328
|
+
}
|
|
329
|
+
verifyRiskToken(token, options) {
|
|
330
|
+
if (!token.startsWith("rt_")) return null;
|
|
331
|
+
const raw = token.slice(3);
|
|
332
|
+
const parts = raw.split(".");
|
|
333
|
+
if (parts.length !== 2) return null;
|
|
334
|
+
const [body, sig] = parts;
|
|
335
|
+
if (!body || !sig) return null;
|
|
336
|
+
const expected = b64url(crypto.createHmac("sha256", this.secret).update(body).digest());
|
|
337
|
+
const a = Buffer.from(sig);
|
|
338
|
+
const b = Buffer.from(expected);
|
|
339
|
+
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;
|
|
340
|
+
try {
|
|
341
|
+
const payload = JSON.parse(fromB64url(body).toString("utf8"));
|
|
342
|
+
if (typeof payload.requestId !== "string" || typeof payload.siteId !== "string" || typeof payload.sessionId !== "string" || typeof payload.decision !== "string" || typeof payload.issuedAt !== "number" || typeof payload.expiresAt !== "number") {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
if (now >= payload.expiresAt) return null;
|
|
347
|
+
if (options?.expectedAction && payload.action !== options.expectedAction) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
if (options?.maxAgeMs != null && now - payload.issuedAt > options.maxAgeMs) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
return payload;
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// src/BotDetectionServer.ts
|
|
361
|
+
function debugLog(enabled, ...args) {
|
|
362
|
+
if (enabled) console.debug("[bot-detector-server]", ...args);
|
|
363
|
+
}
|
|
364
|
+
var BotDetectionServer = class {
|
|
365
|
+
constructor(options) {
|
|
366
|
+
if (!options.secret || options.secret.length < 16) {
|
|
367
|
+
throw new Error("BotDetectionServer requires secret (>= 16 chars)");
|
|
368
|
+
}
|
|
369
|
+
this.sessionDuration = options.sessionDuration ?? devnBotDetectorCore.DEFAULT_SESSION_DURATION_MS;
|
|
370
|
+
this.thresholds = devnBotDetectorCore.mergeRiskThresholds(options.riskThresholds);
|
|
371
|
+
this.actionThresholds = options.actionThresholds;
|
|
372
|
+
this.ipAllowlist = options.ipAllowlist ?? [];
|
|
373
|
+
this.ipDenylist = options.ipDenylist ?? [];
|
|
374
|
+
this.tokenRevocationStore = options.tokenRevocationStore;
|
|
375
|
+
this.enableRiskToken = options.enableRiskToken ?? true;
|
|
376
|
+
this.riskTokenDuration = options.riskTokenDuration ?? devnBotDetectorCore.DEFAULT_RISK_TOKEN_DURATION_MS;
|
|
377
|
+
this.trustProxy = options.trustProxy ?? false;
|
|
378
|
+
this.debug = options.debug ?? false;
|
|
379
|
+
this.maxTelemetryBytes = options.maxTelemetryBytes ?? devnBotDetectorCore.MAX_TELEMETRY_BYTES;
|
|
380
|
+
this.siteResolver = options.siteResolver ?? new MemorySiteResolver();
|
|
381
|
+
this.sessionStore = options.sessionStore ?? new MemorySessionStore();
|
|
382
|
+
this.rateLimiter = options.rateLimiter ?? new MemoryRateLimiter();
|
|
383
|
+
this.eventSink = options.eventSink;
|
|
384
|
+
this.tokens = new TokenManager(options.secret);
|
|
385
|
+
this.engine = new RiskEngine({ detectorWeights: options.detectorWeights });
|
|
386
|
+
this.customScoring = options.customScoring;
|
|
387
|
+
this.rateLimits = {
|
|
388
|
+
sitePerMinute: options.rateLimits?.sitePerMinute ?? 600,
|
|
389
|
+
sessionPerMinute: options.rateLimits?.sessionPerMinute ?? 60,
|
|
390
|
+
ipPerMinute: options.rateLimits?.ipPerMinute ?? 120
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
async getSite(siteKey) {
|
|
394
|
+
return this.siteResolver.resolveSite(siteKey);
|
|
395
|
+
}
|
|
396
|
+
validateHostname(hostname, site, request) {
|
|
397
|
+
const host = devnBotDetectorCore.normalizeHostname(hostname);
|
|
398
|
+
if (!devnBotDetectorCore.hostnameAllowed(host, site.allowedDomains)) return false;
|
|
399
|
+
const originHost = devnBotDetectorCore.extractHostnameFromOrigin(request?.origin);
|
|
400
|
+
if (originHost && !devnBotDetectorCore.hostnameAllowed(originHost, site.allowedDomains)) return false;
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
async createSession(input) {
|
|
404
|
+
const requestId = secureId("req");
|
|
405
|
+
const parsed = devnBotDetectorCore.validateCreateSessionRequest(input);
|
|
406
|
+
if (!parsed.ok) {
|
|
407
|
+
throw new devnBotDetectorCore.BotDetectorError("MALFORMED_REQUEST", parsed.message, requestId);
|
|
408
|
+
}
|
|
409
|
+
const site = await this.resolveActiveSite(input.siteKey, requestId);
|
|
410
|
+
if (!this.validateHostname(parsed.value.hostname, site, input.request)) {
|
|
411
|
+
throw new devnBotDetectorCore.BotDetectorError(
|
|
412
|
+
"INVALID_HOSTNAME",
|
|
413
|
+
"Hostname is not allowed for this site.",
|
|
414
|
+
requestId
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const ip = this.resolveIp(input.request);
|
|
418
|
+
if (ip && devnBotDetectorCore.ipMatches(ip, this.ipDenylist)) {
|
|
419
|
+
throw new devnBotDetectorCore.BotDetectorError("IP_BLOCKED", "Access denied from this IP.", requestId);
|
|
420
|
+
}
|
|
421
|
+
await this.assertRateLimits(input.siteKey, void 0, input.request, requestId);
|
|
422
|
+
const now = Date.now();
|
|
423
|
+
const sessionId = secureId("ses");
|
|
424
|
+
const nonce = secureId("nonce", 12);
|
|
425
|
+
const expiresAt = now + this.sessionDuration;
|
|
426
|
+
const session = {
|
|
427
|
+
sessionId,
|
|
428
|
+
siteKey: input.siteKey,
|
|
429
|
+
siteId: site.siteId,
|
|
430
|
+
tenantId: site.tenantId,
|
|
431
|
+
nonce,
|
|
432
|
+
createdAt: now,
|
|
433
|
+
expiresAt,
|
|
434
|
+
hostname: devnBotDetectorCore.normalizeHostname(parsed.value.hostname),
|
|
435
|
+
sequence: 0,
|
|
436
|
+
usedNonces: []
|
|
437
|
+
};
|
|
438
|
+
await this.sessionStore.create(session);
|
|
439
|
+
await this.eventSink?.onSessionCreated?.(session);
|
|
440
|
+
debugLog(this.debug, "session created", { sessionId, siteId: site.siteId });
|
|
441
|
+
return {
|
|
442
|
+
sessionId,
|
|
443
|
+
nonce,
|
|
444
|
+
expiresAt: new Date(expiresAt).toISOString()
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async analyze(input) {
|
|
448
|
+
const requestId = secureId("req");
|
|
449
|
+
const parsed = devnBotDetectorCore.validateAnalyzeRequest(input, this.maxTelemetryBytes);
|
|
450
|
+
if (!parsed.ok) {
|
|
451
|
+
const code = parsed.message.includes("bytes") || parsed.message.includes("forbidden") ? "INVALID_TELEMETRY" : "MALFORMED_REQUEST";
|
|
452
|
+
throw new devnBotDetectorCore.BotDetectorError(code, parsed.message, requestId);
|
|
453
|
+
}
|
|
454
|
+
const site = await this.resolveActiveSite(input.siteKey, requestId);
|
|
455
|
+
if (!this.validateHostname(parsed.value.hostname, site, input.request)) {
|
|
456
|
+
throw new devnBotDetectorCore.BotDetectorError(
|
|
457
|
+
"INVALID_HOSTNAME",
|
|
458
|
+
"Hostname is not allowed for this site.",
|
|
459
|
+
requestId
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
const session = await this.sessionStore.get(parsed.value.sessionId);
|
|
463
|
+
if (!session || session.siteKey !== input.siteKey) {
|
|
464
|
+
throw new devnBotDetectorCore.BotDetectorError("INVALID_SESSION", "Invalid or unknown session.", requestId);
|
|
465
|
+
}
|
|
466
|
+
if (Date.now() >= session.expiresAt) {
|
|
467
|
+
await this.sessionStore.invalidate(session.sessionId);
|
|
468
|
+
throw new devnBotDetectorCore.BotDetectorError("SESSION_EXPIRED", "Invalid or expired session.", requestId);
|
|
469
|
+
}
|
|
470
|
+
if (devnBotDetectorCore.normalizeHostname(session.hostname) !== devnBotDetectorCore.normalizeHostname(parsed.value.hostname)) {
|
|
471
|
+
throw new devnBotDetectorCore.BotDetectorError(
|
|
472
|
+
"INVALID_HOSTNAME",
|
|
473
|
+
"Hostname does not match session.",
|
|
474
|
+
requestId
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
if (parsed.value.sequence <= session.sequence) {
|
|
478
|
+
throw new devnBotDetectorCore.BotDetectorError(
|
|
479
|
+
"INVALID_SEQUENCE",
|
|
480
|
+
"Sequence replay or out of order.",
|
|
481
|
+
requestId
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (parsed.value.nonce && session.usedNonces.includes(parsed.value.nonce)) {
|
|
485
|
+
throw new devnBotDetectorCore.BotDetectorError("INVALID_NONCE", "Nonce has already been used.", requestId);
|
|
486
|
+
}
|
|
487
|
+
const ip = this.resolveIp(input.request);
|
|
488
|
+
if (ip && devnBotDetectorCore.ipMatches(ip, this.ipDenylist)) {
|
|
489
|
+
throw new devnBotDetectorCore.BotDetectorError("IP_BLOCKED", "Access denied from this IP.", requestId);
|
|
490
|
+
}
|
|
491
|
+
if (ip && devnBotDetectorCore.ipMatches(ip, this.ipAllowlist)) {
|
|
492
|
+
debugLog(this.debug, "allowlisted IP bypassed risk check", { ip });
|
|
493
|
+
const issuedAt = Date.now();
|
|
494
|
+
const tokenExpires = issuedAt + this.riskTokenDuration;
|
|
495
|
+
let riskToken2;
|
|
496
|
+
let expiresAt2;
|
|
497
|
+
if (this.enableRiskToken) {
|
|
498
|
+
riskToken2 = this.createRiskToken({
|
|
499
|
+
requestId,
|
|
500
|
+
siteId: site.siteId,
|
|
501
|
+
sessionId: session.sessionId,
|
|
502
|
+
decision: "allow",
|
|
503
|
+
issuedAt,
|
|
504
|
+
expiresAt: tokenExpires,
|
|
505
|
+
action: parsed.value.action,
|
|
506
|
+
score: 100
|
|
507
|
+
});
|
|
508
|
+
expiresAt2 = new Date(tokenExpires).toISOString();
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
requestId,
|
|
512
|
+
score: 100,
|
|
513
|
+
confidence: 0.99,
|
|
514
|
+
decision: "allow",
|
|
515
|
+
riskLevel: "low",
|
|
516
|
+
action: parsed.value.action,
|
|
517
|
+
...riskToken2 ? { riskToken: riskToken2 } : {},
|
|
518
|
+
...expiresAt2 ? { expiresAt: expiresAt2 } : {},
|
|
519
|
+
timestamp: Date.now()
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
await this.assertRateLimits(input.siteKey, session.sessionId, input.request, requestId);
|
|
523
|
+
const evaluation = await this.calculateRisk({
|
|
524
|
+
telemetry: parsed.value.telemetry,
|
|
525
|
+
request: this.normalizeRequest(input.request),
|
|
526
|
+
sessionAgeMs: Date.now() - session.createdAt,
|
|
527
|
+
sequence: parsed.value.sequence,
|
|
528
|
+
action: parsed.value.action,
|
|
529
|
+
metadata: parsed.value.metadata
|
|
530
|
+
});
|
|
531
|
+
const effectiveThresholds = parsed.value.action && this.actionThresholds?.[parsed.value.action] ? devnBotDetectorCore.mergeRiskThresholds(this.actionThresholds[parsed.value.action]) : this.thresholds;
|
|
532
|
+
const decision = evaluation.decision ?? devnBotDetectorCore.decisionFromScore(evaluation.score, effectiveThresholds);
|
|
533
|
+
const riskLevel = evaluation.riskLevel ?? devnBotDetectorCore.riskLevelFromScore(evaluation.score);
|
|
534
|
+
let riskToken;
|
|
535
|
+
let expiresAt;
|
|
536
|
+
if (this.enableRiskToken) {
|
|
537
|
+
const issuedAt = Date.now();
|
|
538
|
+
const tokenExpires = issuedAt + this.riskTokenDuration;
|
|
539
|
+
riskToken = this.createRiskToken({
|
|
540
|
+
requestId,
|
|
541
|
+
siteId: site.siteId,
|
|
542
|
+
sessionId: session.sessionId,
|
|
543
|
+
decision,
|
|
544
|
+
issuedAt,
|
|
545
|
+
expiresAt: tokenExpires,
|
|
546
|
+
action: parsed.value.action,
|
|
547
|
+
score: evaluation.score
|
|
548
|
+
});
|
|
549
|
+
expiresAt = new Date(tokenExpires).toISOString();
|
|
550
|
+
}
|
|
551
|
+
const usedNonces = [...session.usedNonces];
|
|
552
|
+
if (parsed.value.nonce) {
|
|
553
|
+
usedNonces.push(parsed.value.nonce);
|
|
554
|
+
if (usedNonces.length > 32) usedNonces.splice(0, usedNonces.length - 32);
|
|
555
|
+
}
|
|
556
|
+
await this.sessionStore.update({
|
|
557
|
+
...session,
|
|
558
|
+
sequence: parsed.value.sequence,
|
|
559
|
+
usedNonces,
|
|
560
|
+
nonce: secureId("nonce", 12)
|
|
561
|
+
});
|
|
562
|
+
const result = {
|
|
563
|
+
requestId,
|
|
564
|
+
score: evaluation.score,
|
|
565
|
+
confidence: evaluation.confidence,
|
|
566
|
+
decision,
|
|
567
|
+
riskLevel,
|
|
568
|
+
action: parsed.value.action,
|
|
569
|
+
...riskToken ? { riskToken } : {},
|
|
570
|
+
...expiresAt ? { expiresAt } : {},
|
|
571
|
+
timestamp: Date.now()
|
|
572
|
+
};
|
|
573
|
+
await this.eventSink?.onAnalysis?.(result);
|
|
574
|
+
debugLog(this.debug, "analyzed", {
|
|
575
|
+
requestId,
|
|
576
|
+
decision: result.decision,
|
|
577
|
+
score: result.score,
|
|
578
|
+
action: result.action
|
|
579
|
+
});
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
async calculateRisk(input) {
|
|
583
|
+
const base = this.engine.evaluate(input);
|
|
584
|
+
if (!this.customScoring) return base;
|
|
585
|
+
const custom = await this.customScoring(input);
|
|
586
|
+
const riskFromCustom = custom.risk ?? 0;
|
|
587
|
+
let score = base.score - riskFromCustom;
|
|
588
|
+
if (typeof custom.score === "number") score = custom.score;
|
|
589
|
+
score = Math.max(0, Math.min(100, Math.round(score)));
|
|
590
|
+
const internalReasons = [
|
|
591
|
+
...base.internalReasons,
|
|
592
|
+
...custom.internalReasons ?? [],
|
|
593
|
+
...riskFromCustom ? [{ code: "CUSTOM_SCORING", weight: riskFromCustom }] : []
|
|
594
|
+
];
|
|
595
|
+
return {
|
|
596
|
+
score,
|
|
597
|
+
confidence: custom.confidence ?? base.confidence,
|
|
598
|
+
riskLevel: custom.riskLevel ?? devnBotDetectorCore.riskLevelFromScore(score),
|
|
599
|
+
decision: custom.decision,
|
|
600
|
+
internalReasons
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
createRiskToken(payload) {
|
|
604
|
+
return this.tokens.createRiskToken(payload);
|
|
605
|
+
}
|
|
606
|
+
verifyRiskToken(token, options) {
|
|
607
|
+
return this.tokens.verifyRiskToken(token, options);
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Validates risk token and consumes it (one-time use) if tokenRevocationStore is configured.
|
|
611
|
+
* Throws TOKEN_ALREADY_USED if already consumed or MALFORMED_REQUEST if invalid/expired.
|
|
612
|
+
*/
|
|
613
|
+
async consumeRiskToken(token, options) {
|
|
614
|
+
const requestId = secureId("req");
|
|
615
|
+
const payload = this.verifyRiskToken(token, options);
|
|
616
|
+
if (!payload) {
|
|
617
|
+
throw new devnBotDetectorCore.BotDetectorError("MALFORMED_REQUEST", "Invalid or expired risk token.", requestId);
|
|
618
|
+
}
|
|
619
|
+
if (this.tokenRevocationStore) {
|
|
620
|
+
const revoked = await this.tokenRevocationStore.isRevoked(token);
|
|
621
|
+
if (revoked) {
|
|
622
|
+
throw new devnBotDetectorCore.BotDetectorError(
|
|
623
|
+
"TOKEN_ALREADY_USED",
|
|
624
|
+
"Risk token has already been consumed.",
|
|
625
|
+
requestId
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
const remainingTtl = Math.max(1e3, payload.expiresAt - Date.now());
|
|
629
|
+
await this.tokenRevocationStore.revoke(token, remainingTtl);
|
|
630
|
+
}
|
|
631
|
+
return payload;
|
|
632
|
+
}
|
|
633
|
+
async resolveActiveSite(siteKey, requestId) {
|
|
634
|
+
const site = await this.siteResolver.resolveSite(siteKey);
|
|
635
|
+
if (!site || site.status !== "active") {
|
|
636
|
+
throw new devnBotDetectorCore.BotDetectorError("INVALID_SITE_KEY", "Invalid or inactive site key.", requestId);
|
|
637
|
+
}
|
|
638
|
+
return site;
|
|
639
|
+
}
|
|
640
|
+
async assertRateLimits(siteKey, sessionId, request, requestId) {
|
|
641
|
+
const site = await this.rateLimiter.check(
|
|
642
|
+
`site:${siteKey}`,
|
|
643
|
+
this.rateLimits.sitePerMinute,
|
|
644
|
+
6e4
|
|
645
|
+
);
|
|
646
|
+
if (!site.allowed) {
|
|
647
|
+
throw new devnBotDetectorCore.BotDetectorError("RATE_LIMITED", "Rate limit exceeded.", requestId);
|
|
648
|
+
}
|
|
649
|
+
if (sessionId) {
|
|
650
|
+
const sess = await this.rateLimiter.check(
|
|
651
|
+
`session:${sessionId}`,
|
|
652
|
+
this.rateLimits.sessionPerMinute,
|
|
653
|
+
6e4
|
|
654
|
+
);
|
|
655
|
+
if (!sess.allowed) {
|
|
656
|
+
throw new devnBotDetectorCore.BotDetectorError("RATE_LIMITED", "Rate limit exceeded.", requestId);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const ip = this.resolveIp(request);
|
|
660
|
+
if (ip) {
|
|
661
|
+
const ipLimit = await this.rateLimiter.check(`ip:${ip}`, this.rateLimits.ipPerMinute, 6e4);
|
|
662
|
+
if (!ipLimit.allowed) {
|
|
663
|
+
throw new devnBotDetectorCore.BotDetectorError("RATE_LIMITED", "Rate limit exceeded.", requestId);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
resolveIp(request) {
|
|
668
|
+
if (!request) return void 0;
|
|
669
|
+
if (this.trustProxy && request.forwardedFor) {
|
|
670
|
+
return request.forwardedFor.split(",")[0]?.trim();
|
|
671
|
+
}
|
|
672
|
+
return request.ip;
|
|
673
|
+
}
|
|
674
|
+
normalizeRequest(request) {
|
|
675
|
+
if (!request) return {};
|
|
676
|
+
return {
|
|
677
|
+
...request.ip ? { ip: request.ip } : {},
|
|
678
|
+
...request.userAgent ? { userAgent: request.userAgent } : {},
|
|
679
|
+
...request.origin ? { origin: request.origin } : {},
|
|
680
|
+
...request.host ? { host: request.host } : {},
|
|
681
|
+
...this.trustProxy && request.forwardedFor ? { forwardedFor: request.forwardedFor } : {}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// src/adapters/redis.ts
|
|
687
|
+
var RedisSessionStore = class {
|
|
688
|
+
constructor(client, options) {
|
|
689
|
+
this.client = client;
|
|
690
|
+
this.prefix = options?.prefix ?? "bd:sess:";
|
|
691
|
+
this.defaultTtl = options?.defaultTtlSeconds ?? 1800;
|
|
692
|
+
}
|
|
693
|
+
key(sessionId) {
|
|
694
|
+
return `${this.prefix}${sessionId}`;
|
|
695
|
+
}
|
|
696
|
+
async create(session) {
|
|
697
|
+
const remaining = Math.ceil((session.expiresAt - Date.now()) / 1e3);
|
|
698
|
+
const ttl = remaining > 0 ? remaining : this.defaultTtl;
|
|
699
|
+
await this.client.set(this.key(session.sessionId), JSON.stringify(session), "EX", ttl);
|
|
700
|
+
}
|
|
701
|
+
async get(sessionId) {
|
|
702
|
+
const data = await this.client.get(this.key(sessionId));
|
|
703
|
+
if (!data) return null;
|
|
704
|
+
try {
|
|
705
|
+
return JSON.parse(data);
|
|
706
|
+
} catch {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async update(session) {
|
|
711
|
+
const remaining = Math.ceil((session.expiresAt - Date.now()) / 1e3);
|
|
712
|
+
const ttl = remaining > 0 ? remaining : this.defaultTtl;
|
|
713
|
+
await this.client.set(this.key(session.sessionId), JSON.stringify(session), "EX", ttl);
|
|
714
|
+
}
|
|
715
|
+
async invalidate(sessionId) {
|
|
716
|
+
await this.client.del(this.key(sessionId));
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
var RedisRateLimiter = class {
|
|
720
|
+
constructor(client, options) {
|
|
721
|
+
this.client = client;
|
|
722
|
+
this.prefix = options?.prefix ?? "bd:rl:";
|
|
723
|
+
}
|
|
724
|
+
async check(key, limit, windowMs) {
|
|
725
|
+
const fullKey = `${this.prefix}${key}`;
|
|
726
|
+
const windowSec = Math.max(1, Math.ceil(windowMs / 1e3));
|
|
727
|
+
try {
|
|
728
|
+
if (typeof this.client.eval === "function") {
|
|
729
|
+
const lua = `
|
|
730
|
+
local current = redis.call('INCR', KEYS[1])
|
|
731
|
+
if current == 1 then
|
|
732
|
+
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
|
733
|
+
end
|
|
734
|
+
return current
|
|
735
|
+
`;
|
|
736
|
+
const count2 = Number(await this.client.eval(lua, 1, fullKey, windowSec));
|
|
737
|
+
const allowed2 = count2 <= limit;
|
|
738
|
+
const remaining = Math.max(0, limit - count2);
|
|
739
|
+
return {
|
|
740
|
+
allowed: allowed2,
|
|
741
|
+
remaining,
|
|
742
|
+
...allowed2 ? {} : { retryAfter: windowSec }
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
const raw = await this.client.get(fullKey);
|
|
746
|
+
const count = (raw ? parseInt(raw, 10) : 0) + 1;
|
|
747
|
+
await this.client.set(fullKey, String(count), "EX", windowSec);
|
|
748
|
+
const allowed = count <= limit;
|
|
749
|
+
return {
|
|
750
|
+
allowed,
|
|
751
|
+
remaining: Math.max(0, limit - count),
|
|
752
|
+
...allowed ? {} : { retryAfter: windowSec }
|
|
753
|
+
};
|
|
754
|
+
} catch {
|
|
755
|
+
return { allowed: true, remaining: limit };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
var RedisTokenRevocationStore = class {
|
|
760
|
+
constructor(client, options) {
|
|
761
|
+
this.client = client;
|
|
762
|
+
this.prefix = options?.prefix ?? "bd:revoked:";
|
|
763
|
+
}
|
|
764
|
+
async isRevoked(tokenId) {
|
|
765
|
+
const exists = await this.client.get(`${this.prefix}${tokenId}`);
|
|
766
|
+
return exists !== null;
|
|
767
|
+
}
|
|
768
|
+
async revoke(tokenId, ttlMs) {
|
|
769
|
+
const ttlSec = Math.max(1, Math.ceil(ttlMs / 1e3));
|
|
770
|
+
await this.client.set(`${this.prefix}${tokenId}`, "1", "EX", ttlSec);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
async function verifyRequestRiskToken(server, request, options) {
|
|
774
|
+
const headerName = (options?.headerName ?? "x-risk-token").toLowerCase();
|
|
775
|
+
const bodyField = options?.bodyField ?? "riskToken";
|
|
776
|
+
let token;
|
|
777
|
+
if (typeof request.header === "function") {
|
|
778
|
+
token = request.header(headerName);
|
|
779
|
+
} else if (typeof request.get === "function") {
|
|
780
|
+
token = request.get(headerName);
|
|
781
|
+
} else if (request.headers) {
|
|
782
|
+
const raw = request.headers[headerName];
|
|
783
|
+
token = Array.isArray(raw) ? raw[0] : raw;
|
|
784
|
+
}
|
|
785
|
+
if (!token && request.body && typeof request.body === "object") {
|
|
786
|
+
const fromBody = request.body[bodyField];
|
|
787
|
+
if (typeof fromBody === "string") token = fromBody;
|
|
788
|
+
}
|
|
789
|
+
if (!token) {
|
|
790
|
+
throw new devnBotDetectorCore.BotDetectorError(
|
|
791
|
+
"MALFORMED_REQUEST",
|
|
792
|
+
`Missing risk token in header '${headerName}' or body field '${bodyField}'.`,
|
|
793
|
+
"req_guard"
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
if (options?.consume) {
|
|
797
|
+
return server.consumeRiskToken(token, options);
|
|
798
|
+
}
|
|
799
|
+
const payload = server.verifyRiskToken(token, options);
|
|
800
|
+
if (!payload) {
|
|
801
|
+
throw new devnBotDetectorCore.BotDetectorError("MALFORMED_REQUEST", "Invalid or expired risk token.", "req_guard");
|
|
802
|
+
}
|
|
803
|
+
return payload;
|
|
804
|
+
}
|
|
805
|
+
function createExpressBotDetector(server) {
|
|
806
|
+
return {
|
|
807
|
+
/** Route handler for POST /v1/session */
|
|
808
|
+
sessionHandler: async (req, res) => {
|
|
809
|
+
try {
|
|
810
|
+
const siteKey = String(req.header(devnBotDetectorCore.SITE_KEY_HEADER) || req.headers[devnBotDetectorCore.SITE_KEY_HEADER.toLowerCase()] || "");
|
|
811
|
+
const result = await server.createSession({
|
|
812
|
+
siteKey,
|
|
813
|
+
hostname: req.body?.hostname,
|
|
814
|
+
userAgent: req.body?.userAgent,
|
|
815
|
+
timestamp: req.body?.timestamp,
|
|
816
|
+
request: {
|
|
817
|
+
ip: req.ip,
|
|
818
|
+
userAgent: req.get?.("user-agent"),
|
|
819
|
+
origin: req.get?.("origin"),
|
|
820
|
+
host: req.get?.("host"),
|
|
821
|
+
forwardedFor: req.get?.("x-forwarded-for")
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
res.json(result);
|
|
825
|
+
} catch (err) {
|
|
826
|
+
if (err instanceof devnBotDetectorCore.BotDetectorError) {
|
|
827
|
+
res.status(err.code === "RATE_LIMITED" ? 429 : err.code === "IP_BLOCKED" ? 403 : 400).json(err.toJSON());
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
res.status(500).json({ error: { code: "INTERNAL_ERROR", message: "Internal error.", requestId: "req_err" } });
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
/** Route handler for POST /v1/analyze */
|
|
834
|
+
analyzeHandler: async (req, res) => {
|
|
835
|
+
try {
|
|
836
|
+
const siteKey = String(req.header(devnBotDetectorCore.SITE_KEY_HEADER) || req.headers[devnBotDetectorCore.SITE_KEY_HEADER.toLowerCase()] || "");
|
|
837
|
+
const result = await server.analyze({
|
|
838
|
+
siteKey,
|
|
839
|
+
...req.body,
|
|
840
|
+
request: {
|
|
841
|
+
ip: req.ip,
|
|
842
|
+
userAgent: req.get?.("user-agent"),
|
|
843
|
+
origin: req.get?.("origin"),
|
|
844
|
+
host: req.get?.("host"),
|
|
845
|
+
forwardedFor: req.get?.("x-forwarded-for")
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
res.json(result);
|
|
849
|
+
} catch (err) {
|
|
850
|
+
if (err instanceof devnBotDetectorCore.BotDetectorError) {
|
|
851
|
+
res.status(err.code === "RATE_LIMITED" ? 429 : err.code === "IP_BLOCKED" ? 403 : 400).json(err.toJSON());
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
res.status(500).json({ error: { code: "INTERNAL_ERROR", message: "Internal error.", requestId: "req_err" } });
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
/** Middleware protecting sensitive routes with risk token verification */
|
|
858
|
+
requireRiskToken: (options) => {
|
|
859
|
+
return async (req, res, next) => {
|
|
860
|
+
try {
|
|
861
|
+
const payload = await verifyRequestRiskToken(server, req, options);
|
|
862
|
+
req.riskTokenPayload = payload;
|
|
863
|
+
next();
|
|
864
|
+
} catch (err) {
|
|
865
|
+
if (err instanceof devnBotDetectorCore.BotDetectorError) {
|
|
866
|
+
res.status(err.code === "TOKEN_ALREADY_USED" ? 409 : 403).json(err.toJSON());
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
res.status(403).json({ error: { code: "MALFORMED_REQUEST", message: "Forbidden", requestId: "req_guard" } });
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
Object.defineProperty(exports, "API_PATHS", {
|
|
877
|
+
enumerable: true,
|
|
878
|
+
get: function () { return devnBotDetectorCore.API_PATHS; }
|
|
879
|
+
});
|
|
880
|
+
Object.defineProperty(exports, "BotDetectorError", {
|
|
881
|
+
enumerable: true,
|
|
882
|
+
get: function () { return devnBotDetectorCore.BotDetectorError; }
|
|
883
|
+
});
|
|
884
|
+
Object.defineProperty(exports, "SITE_KEY_HEADER", {
|
|
885
|
+
enumerable: true,
|
|
886
|
+
get: function () { return devnBotDetectorCore.SITE_KEY_HEADER; }
|
|
887
|
+
});
|
|
888
|
+
exports.ActionRiskDetector = ActionRiskDetector;
|
|
889
|
+
exports.AutomationDetector = AutomationDetector;
|
|
890
|
+
exports.BotDetectionServer = BotDetectionServer;
|
|
891
|
+
exports.BrowserConsistencyDetector = BrowserConsistencyDetector;
|
|
892
|
+
exports.DEFAULT_DETECTORS = DEFAULT_DETECTORS;
|
|
893
|
+
exports.EntropyDetector = EntropyDetector;
|
|
894
|
+
exports.HeadlessAdvancedDetector = HeadlessAdvancedDetector;
|
|
895
|
+
exports.InteractionDetector = InteractionDetector;
|
|
896
|
+
exports.MemoryRateLimiter = MemoryRateLimiter;
|
|
897
|
+
exports.MemorySessionStore = MemorySessionStore;
|
|
898
|
+
exports.MemorySiteResolver = MemorySiteResolver;
|
|
899
|
+
exports.MemoryTokenRevocationStore = MemoryTokenRevocationStore;
|
|
900
|
+
exports.RedisRateLimiter = RedisRateLimiter;
|
|
901
|
+
exports.RedisSessionStore = RedisSessionStore;
|
|
902
|
+
exports.RedisTokenRevocationStore = RedisTokenRevocationStore;
|
|
903
|
+
exports.RiskEngine = RiskEngine;
|
|
904
|
+
exports.SessionDetector = SessionDetector;
|
|
905
|
+
exports.TimingDetector = TimingDetector;
|
|
906
|
+
exports.TokenManager = TokenManager;
|
|
907
|
+
exports.VelocityDetector = VelocityDetector;
|
|
908
|
+
exports.createExpressBotDetector = createExpressBotDetector;
|
|
909
|
+
exports.secureId = secureId;
|
|
910
|
+
exports.verifyRequestRiskToken = verifyRequestRiskToken;
|
|
911
|
+
//# sourceMappingURL=index.cjs.map
|
|
912
|
+
//# sourceMappingURL=index.cjs.map
|