Core

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:

Install

1
npm i @botbye/node-core
or
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"