# @botpoison/node

## Install

npm install @botpoison/node

It needs Node.js 18 or later and has no dependencies. Load it with `import` or `require`: both return the class. `classify()` needs version 0.2.0 or later.

## Creating an instance

Create an instance with your project's secret key, read from your server's environment. The same instance verifies solutions and classifies messages, because one key works with every product.

JavaScript

```javascript
import Botpoison from "@botpoison/node";

// or const Botpoison = require("@botpoison/node")

// One instance for every product, with the project's secret key read from
// your server's environment.
const botpoison = new Botpoison({
  secretKey: process.env.BOTPOISON_SECRET_KEY
});
```

## verify(solution)

Verifies the `_botpoison` field a Challenge form sent. It resolves to `{ ok }`: accept the form when `ok` is `true`, and reject it otherwise.

JavaScript

```javascript
// 1. Import the library
import Botpoison from "@botpoison/node";

// or const Botpoison = require("@botpoison/node")

// 2. Create a new instance with your secret key, read from your server's
// environment: BOTPOISON_SECRET_KEY=sk_xxxxxxxx
const botpoison = new Botpoison({
  secretKey: process.env.BOTPOISON_SECRET_KEY
});

server.post("/message", async (req, res) => {
  const { message, _botpoison } = req.body;
  // 3. Verify the solution
  const { ok } = await botpoison.verify(_botpoison);
  if (!ok) {
    throw new Error("Invalid Botpoison solution");
  }
  // ...
});
```

It sends the key as `secretKey` in the body. It sets no timeout of its own, so wrap the call in one. A fault on our side answers `ok: false` with the message `Internal server error`. See [If Botpoison is unreachable](https://botpoison.com/documentation/challenge/rest-api/#unreachable) for what to accept during an outage.

## classify(content, options)

Sends a submission to Classify and resolves to a verdict: `{ label: "spam" | "clean", score, model }`. It sends the key as an `Authorization: Bearer` header, and the body holds only the content and the model.

JavaScript

```javascript
// 1. Import the library
import Botpoison from "@botpoison/node";

// or const Botpoison = require("@botpoison/node")

// 2. Create a new instance with your secret key, read from your server's
// environment: BOTPOISON_SECRET_KEY=sk_xxxxxxxx
const botpoison = new Botpoison({
  secretKey: process.env.BOTPOISON_SECRET_KEY
});

server.post("/contact", async (req, res) => {
  // 3. Classify the whole form and read the label
  const { label } = await botpoison.classify(req.body, {
    model: "spam-standard",
    timeout: 8000,
  });
  if (label === "spam") {
    // Drop it, or file it away
    return res.status(202).end();
  }
  // ...
});
```

Act on `label`, which is `spam` from a score of 0.8. `model` names the model that answered. See [Labels and scores](https://botpoison.com/documentation/classify/labels-and-scores/).

### Content

The first argument is the content: the whole form as an object, such as `req.body`, or a string for a single message box. Send the whole form when you have it, since every field helps the verdict. A field's value may be a string, a number, a boolean, null, or a list of strings, numbers and booleans. A nested object is refused. Fields whose name starts with `_` are ignored, so form controls such as `_redirect` can stay in. See the [REST API](https://botpoison.com/documentation/classify/rest-api/) for the limits.

### Options

The second argument is required, since it names the model.

| Parameter | Type | Description |
| --- | --- | --- |
| model Required | string | The model to classify with, by name: spam-standard is the only model today. |
| timeout Optional | number | Milliseconds from the start of the call until it is abandoned, however the time is spent. Defaults to 8000. |

An unknown model is rejected with a 400 that lists the valid models. See [Models](https://botpoison.com/documentation/classify/models/).

### Without the library

Node.js 18 and later ship `fetch`, so you can call the REST API directly instead.

JavaScript

```javascript
// Node.js 18 and later ship fetch, so there is nothing to install.
// The secret key comes from your server's environment:
// BOTPOISON_SECRET_KEY=sk_xxxxxxxx
const SECRET_KEY = process.env.BOTPOISON_SECRET_KEY;

// 1. Send the submission to Classify
const classify = async (content, model = "spam-standard") => {
  const response = await fetch("https://api.botpoison.com/classify", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${SECRET_KEY}`,
    },
    body: JSON.stringify({ content, model }),
  });
  // A refusal carries a JSON message, but a proxy in between may answer
  // with something else, so the status is checked before the body is parsed.
  if (!response.ok) {
    const { message } = await response.json().catch(() => ({}));
    throw new Error(message ?? `Classify answered ${response.status}`);
  }
  // { label: "spam" | "clean", score, model }
  return response.json();
};

server.post("/contact", async (req, res) => {
  // 2. Send the whole form, then read the label
  const { label } = await classify(req.body);
  if (label === "spam") {
    // Drop it, or file it away
    return res.status(202).end();
  }
  // ...
});
```

## Errors

### From verify()

`verify()` answers a rejected solution with `ok: false` rather than an error. It rejects when the request fails before an answer arrives, with the parsed response body when the API answers a status other than 200, and with an `Error` when the response is not JSON or a 200 carries no `ok`.

### From classify()

`classify()` rejects with a `Botpoison.ClassifyError`. Its `code` says what went wrong, its `status` carries the HTTP status when Classify answered, its `body` carries the parsed response body, and its `message` says why.

| Code | Meaning |
| --- | --- |
| `http` | Classify answered with an error status. status is that status, and message is the reason it sent. |
| `timeout` | No answer arrived within the timeout. |
| `network` | The request failed before an answer arrived. |
| `invalid_response` | Classify answered 200 with a body that is not a classification. |

JavaScript

```javascript
try {
  const { label } = await botpoison.classify(req.body, {
    model: "spam-standard",
  });
  // ...
} catch (error) {
  if (error instanceof Botpoison.ClassifyError) {
    // error.code is "http", "timeout", "network" or "invalid_response".
    // error.status is the HTTP status when Classify answered.
    console.error(error.code, error.status, error.message);
  }
  // Deliver the message as clean, so an outage never loses a real one
}
```

Decide what a failure means for your form. If you deliver the submission as `clean`, an outage never loses a real one. Three statuses are worth handling apart.

| Status | Meaning |
| --- | --- |
| 401 | The key is missing, unknown or rerolled. Fix the key in your environment rather than retrying. |
| 429 | The key is past its rate limit, or a Free workspace has paused after its allowance. Wait as long as Retry-After says. |
| 503 | The model is unavailable and the call is not counted. Retry later with backoff. |

Every status code is listed in the [REST API reference](https://botpoison.com/documentation/classify/rest-api/).

## TypeScript

The package ships its own types. Beside the class, it exports these types for the Classify calls: `ClassifyContent`, `ClassifyFields`, `ClassifyFieldValue`, `ClassifyOptions`, `ClassifyResult`, `ClassifyLabel`, `ClassifyError` and `ClassifyErrorCode`. At runtime, the error class is `Botpoison.ClassifyError`.
