Node.js Core
BotBye! core module for Node.js — the low-level building block used by all BotBye framework integrations.
Use this package when no framework-specific integration is available for your environment, or when you need full control over how request information is passed to the SDK.
For most use cases, prefer a framework-specific package:
- @botbye/node-http — Node.js http module
- @botbye/express — Express
- @botbye/fastify — Fastify
- @botbye/cloudflare-worker — Cloudflare Workers
- @botbye/nextjs — Next.js
- @botbye/nuxt — Nuxt
Install
1
npm i @botbye/node-core
1
yarn add @botbye/node-core
Configuration
@botbye/node-core does not export init and evaluate directly. Call moduleApiFactory to create an SDK instance:
1
2
3
4
5
6
7
8
9
10
11
import { moduleApiFactory } from "@botbye/node-core";
import { nodeHttpClient } from "@botbye/node-core/node-http-client";
const { init, evaluate, dev } = moduleApiFactory({
httpClient: nodeHttpClient,
});
init({
// Use your project server-key
serverKey: "00000000-0000-0000-0000-000000000000",
});
Call init once at application startup, before any calls to evaluate.
moduleApiFactory options
| Option | Type | Required | Description |
|---|---|---|---|
| httpClient | THttpClient | Yes | HTTP client used for API calls. |
| requestInfoExtractor | (request: R, global: TGlobalOptions) => TRequestInfo | No | Converts a custom request object into request info, enabling { request: R } in evaluate. |
| url | string | No | Override BotBye API endpoint. Can also be set via init. |
HTTP clients
Two built-in HTTP clients are available:
| Import path | When to use |
|---|---|
| @botbye/node-core/node-http-client | Standard Node.js environments (uses built-in http/https) |
| @botbye/node-core/fetch-http-client | Runtimes with the Fetch API (Deno, Bun, edge runtimes) |
If neither fits (custom proxy, special auth headers, retry logic), implement the THttpClient interface:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import type { THttpClient } from "@botbye/node-core";
const myHttpClient: THttpClient = {
type: "my-client",
call(url, init) {
const controller = new AbortController();
const result = fetch(url, {
method: init.method,
headers: init.headers,
body: JSON.stringify(init.body),
signal: controller.signal,
}).then((r) => r.text());
return { result, abort: () => controller.abort() };
},
};
init options
| Option | Type | Required | Description |
|---|---|---|---|
| serverKey | string | Yes | Server key from your BotBye project |
| url | string | No | Override BotBye API endpoint (default: https://verify.botbye.com) |
| logger.level | "error" "warn" "info" "debug" "log" | No | Log level (default: "info") |
| logger.logger | TLogger | No | Custom logger instance implementing { error, warn, info, debug, log } |
| timeouts.evaluate | number | No | Timeout in milliseconds for each evaluate call |
Building a custom integration
requestInfoExtractor lets you build a first-class integration for any framework not yet covered by an official package. It bridges the gap between a framework's native request object and the TRequestInfo shape that evaluate needs internally.
What it does:
When requestInfoExtractor is provided, evaluate gains a second calling form: instead of passing fields explicitly, you can pass { request: YourRequestObject }. The extractor is called automatically to derive ip, headers, requestMethod, and requestUri from it.
1
2
3
4
5
// Without requestInfoExtractor — explicit fields only
evaluate({ type: "validate", request: { ip, headers, requestMethod, requestUri, token } });
// With requestInfoExtractor — framework request object accepted directly
evaluate({ type: "validate", request: { request: req, token } });
Both forms remain valid side by side. Routes that already pass fields explicitly continue to work.
Token handling:
The extractor may also return a token field (e.g. extracted from a known header). If the caller also passes token in the event, the event's value takes precedence. This lets the extractor provide a sensible default while allowing per-call overrides.
TypeScript:
The generic parameter R on moduleApiFactory<R> flows through to the evaluate signature. Providing requestInfoExtractor is what makes the typed { request: R } form valid:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { moduleApiFactory } from "@botbye/node-core";
import { nodeHttpClient } from "@botbye/node-core/node-http-client";
import type { Request } from "express";
const { init, evaluate, dev } = moduleApiFactory<Request>({
httpClient: nodeHttpClient,
requestInfoExtractor: (req, global) => {
try {
return {
ip: req.ip ?? req.socket.remoteAddress ?? "0.0.0.0",
headers: req.headers as Record<string, string>,
requestMethod: req.method,
requestUri: req.url,
token: req.headers["x-botbye-token"] as string ?? null,
};
} catch {
global.logger.warn("Failed to extract request info from Express request");
return { ip: "0.0.0.0", headers: {} };
}
},
});
init({
// Use your project server-key
serverKey: "00000000-0000-0000-0000-000000000000",
});
// Now evaluate accepts Express Request directly
app.use(async (req, res, next) => {
const result = await evaluate({
type: "validate",
request: {
request: req,
// "x-botbye-token" is an example — pass the token from wherever you store it
token: req.headers["x-botbye-token"] as string,
},
});
if (result.decision === "BLOCK") {
return res.status(403).json({ error: "Forbidden" });
}
next();
});
The global argument passed to the extractor exposes global.logger — use it for warnings when the request object is malformed or unexpected.
Usage
Call evaluate with an event object describing what you know about the request. It returns a promise that resolves to a decision.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function handleRequest(ip, headers, method, url, token) {
const result = await evaluate({
type: "validate",
request: {
ip,
headers,
requestMethod: method,
requestUri: url,
token,
},
});
if (result.decision === "BLOCK") {
return { status: 403 };
}
// proceed normally
}
Without requestInfoExtractor, all request fields must be provided explicitly — there is no framework request object to pass.
There are three event types — validate, risk, and full — each suited for a different layer of your application.
validate — edge-level bot check
Use at the edge — API gateway, route handler, middleware — when you just want to know: was this request made by a bot? No user or domain context needed.
Event fields:
1
2
3
4
5
6
7
8
9
{
type: "validate";
request:
// Option A: custom request object — only available when requestInfoExtractor is configured
| { request: R; token?: string | null }
// Option B: explicit fields
| { ip: string; headers: Record<string, string>; requestMethod?: string | null; requestUri?: string | null; token?: string | null };
customFields?: Record<string, string>;
}
Pass IP and headers extracted from your runtime's request object.
Option A — passing a framework request object directly — requires requestInfoExtractor to be configured in moduleApiFactory.
The token is a one-time token generated by the BotBye client-side SDK that contains information about the user's device.
Pass whatever the client sent; if no token is received, the decision will be "BLOCK" due to an invalid token.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function handleRequest(ip, headers, method, url, token) {
const result = await evaluate({
type: "validate",
request: {
ip,
headers,
requestMethod: method,
requestUri: url,
token,
},
});
if (result.decision === "BLOCK") {
return { status: 403 };
}
// proceed normally
}
risk — domain-level risk scoring
Use inside services that already know the user: auth, payments, account management, etc. The purpose shifts from "is this a bot?" to "is something suspicious happening for this user?" — credential stuffing, account takeover, account sharing, logins from a new geo.
Event fields:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
type: "risk";
request:
// Option A: custom request object — only available when requestInfoExtractor is configured
| { request: R }
// Option B: explicit fields
| { ip: string; headers?: Record<string, string>; requestMethod?: string | null; requestUri?: string | null; token?: string | null };
event: {
type: string;
status: "ATTEMPTED" | "SUCCESSFUL" | "FAILED" | "UNKNOWN";
};
user: {
accountId: string;
username?: string | null;
email?: string | null;
phone?: string | null;
};
customFields?: Record<string, string>;
botbyeResult?: string; // if a validate call was made earlier, pass its result.botbyeResult here to link the requests; omit if there was no prior validate
}
event and user are the key fields here — they define what action is being performed and who is performing it, which is what drives the risk score.
ip is equally important: BotBye tracks which IPs access the account to detect patterns like account sharing, credential stuffing, and suspicious geo logins.
Pass it directly as { ip }, or pass a request object via Option A if requestInfoExtractor is configured.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Inside an auth service, after a login attempt
async function onLoginAttempt({ ip, userId, email, loginSucceeded }) {
const result = await evaluate({
type: "risk",
request: { ip },
event: {
type: "login",
status: loginSucceeded ? "SUCCESSFUL" : "FAILED",
// "SUCCESSFUL" | "FAILED" | "ATTEMPTED" | "UNKNOWN"
},
user: {
accountId: userId,
email,
},
});
if (result.decision === "BLOCK") {
// Lock account, trigger MFA, send alert, etc.
}
}
Linking validate and risk events
When the same request is evaluated at two layers — for example, once at the edge (type: "validate") and then again inside a domain service (type: "risk") — BotBye can link both events and display them as a single event in the dashboard.
Step 1 — edge layer (gateway, middleware, or entry point): run validate and capture botbye_result:
1
2
3
4
5
6
7
8
9
10
11
12
const edgeResult = await evaluate({
type: "validate",
request: {
ip,
headers,
requestMethod: method,
requestUri: url,
token,
},
});
const edgeBotbyeResult = edgeResult.botbye_result;
// Pass edgeBotbyeResult downstream — a request header, function argument, shared context, etc.
Step 2 — domain service (auth, payment, account management): pass it as botbyeResult in the risk call:
1
2
3
4
5
6
7
8
9
10
11
12
13
const riskResult = await evaluate({
type: "risk",
request: { ip },
event: {
type: "login",
status: loginSucceeded ? "SUCCESSFUL" : "FAILED",
},
user: {
accountId: userId,
email,
},
botbyeResult: edgeBotbyeResult,
});
botbye_result is optional in the response — if it is absent, omit botbyeResult and the events will be recorded independently.
full — edge check and domain scoring in one call
Use when you have all context at once: raw request, token, user, and event. A login endpoint is a typical example — it receives the HTTP request and immediately knows the user and outcome.
Event fields:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
type: "full";
request:
// Option A: custom request object — only available when requestInfoExtractor is configured
| { request: R; token?: string | null }
// Option B: explicit fields
| { ip: string; headers: Record<string, string>; requestMethod?: string | null; requestUri?: string | null; token?: string | null };
event: {
type: string;
status: "ATTEMPTED" | "SUCCESSFUL" | "FAILED" | "UNKNOWN";
};
user: {
accountId: string;
username?: string | null;
email?: string | null;
phone?: string | null;
};
customFields?: Record<string, string>;
}
Equivalent to running validate and risk in a single call.
Option A requires requestInfoExtractor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async function handleLogin({ ip, headers, method, url, token, email, password }) {
const user = await findUser(email);
const loginSucceeded = user && (await checkPassword(user, password));
const result = await evaluate({
type: "full",
request: {
ip,
headers,
requestMethod: method,
requestUri: url,
token,
},
event: {
type: "login",
status: loginSucceeded ? "SUCCESSFUL" : "FAILED",
},
user: {
accountId: user?.id ?? "unknown",
email,
},
});
if (result.decision === "BLOCK") {
return { status: 403 };
}
// proceed normally
}
Response
evaluate always returns a Promise<TEvaluationResult>:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type TEvaluationResult =
| {
decision: "ALLOW" | "BLOCK" | "CHALLENGE";
request_id: string;
risk_score: number;
scores: Record<string, number>;
signals: string[];
botbye_result?: string;
// Present when the backend received the request but could not process it (e.g. a malformed
// payload or invalid JSON — such as a client omitting required fields): decision is "BLOCK"
// and signals contain "evaluation_error".
error?: { message: string };
}
| {
decision: "ALLOW";
botbye_result?: string;
// SDK fail-open: connectivity or configuration error (e.g. invalid server key).
error: { message: string };
};
Check result.decision to decide how to handle the request:
- "ALLOW" — request appears legitimate, proceed normally
- "BLOCK" — bot or suspicious activity detected, block the request
- "CHALLENGE" — uncertain, consider issuing a CAPTCHA, MFA, or additional verification step
When the response contains an error field, BotBye could not fully evaluate the request:
- The SDK fails open with decision: "ALLOW" on connectivity or configuration problems (e.g. an invalid server-key), so a misconfiguration does not block real users.
- The backend fails closed with decision: "BLOCK" and signals: ["evaluation_error"] when it receives a request it cannot process — a malformed payload or invalid JSON, such as a client omitting required fields. The error.message names the offending field.
In both cases error.message describes the underlying problem — monitor it and fix the root cause.
Examples of BotBye API responses
Blocked (bot detected):
1
2
3
4
5
6
7
{
"request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"decision": "BLOCK",
"risk_score": 0.95,
"scores": { "bot": 0.95 },
"signals": ["AutomationTool"]
}
Allowed:
1
2
3
4
5
6
7
{
"request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"decision": "ALLOW",
"risk_score": 0.05,
"scores": { "bot": 0.05, "ato": 0.02 },
"signals": []
}
Challenge:
1
2
3
4
5
6
7
8
{
"request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"decision": "CHALLENGE",
"risk_score": 0.65,
"scores": { "bot": 0.65 },
"signals": ["SuspiciousFingerprint"],
"challenge": { "type": "CAPTCHA" }
}
Invalid server-key (SDK fail-open):
1
2
3
4
{
"decision": "ALLOW",
"error": { "message": "[BotBye] Bad Request: Invalid Server Key" }
}
Evaluation error — backend could not process the request (fail-closed):
1
2
3
4
5
6
7
8
{
"request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"decision": "BLOCK",
"risk_score": 1.0,
"scores": {},
"signals": ["evaluation_error"],
"error": { "message": "Invalid request body: missing or invalid field 'user.account_id'" }
}
Advanced: multiple instances
Use moduleApiFactory to create independent SDK instances (useful when protecting multiple projects from one service):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { moduleApiFactory } from "@botbye/node-core";
import { nodeHttpClient } from "@botbye/node-core/node-http-client";
const sdkA = moduleApiFactory({ httpClient: nodeHttpClient });
const sdkB = moduleApiFactory({ httpClient: nodeHttpClient });
sdkA.init({
// Use your project server-key
serverKey: "00000000-0000-0000-0000-000000000000",
});
sdkB.init({
// Use your project server-key
serverKey: "11111111-1111-1111-1111-111111111111",
});
Dev utilities
1
2
3
4
5
6
7
import { moduleApiFactory } from "@botbye/node-core";
import { nodeHttpClient } from "@botbye/node-core/node-http-client";
const { dev } = moduleApiFactory({ httpClient: nodeHttpClient });
// Change log verbosity at runtime
dev.setLoggerLevel("debug"); // "error" | "warn" | "info" | "debug" | "log"