Skip to content

@cap.js/server

@cap.js/server is Cap's server-side library for creating and validating challenges. Install it using your preferred package manager:

bash
bun add @cap.js/server
bash
npm i @cap.js/server
bash
pnpm i @cap.js/server

Getting started

The best way to use Cap is with storage hooks that connect to your database. Here's a simple example with SQLite:

js
import Cap from "@cap.js/server";
import { Database } from "bun:sqlite";

const db = new Database("cap.db");

db.exec(`
  CREATE TABLE IF NOT EXISTS challenges (
    token TEXT PRIMARY KEY,
    data TEXT NOT NULL,
    expires INTEGER NOT NULL
  );
  CREATE TABLE IF NOT EXISTS tokens (
    key TEXT PRIMARY KEY,
    expires INTEGER NOT NULL
  );
`);

const cap = new Cap({
  storage: {
    challenges: {
      store: async (token, challengeData) => {
        db.prepare(
          "INSERT OR REPLACE INTO challenges (token, data, expires) VALUES (?, ?, ?)"
        ).run(token, JSON.stringify(challengeData), challengeData.expires);
      },
      read: async (token) => {
        const row = db
          .prepare(
            "SELECT data, expires FROM challenges WHERE token = ? AND expires > ?"
          )
          .get(token, Date.now());

        return row
          ? { challenge: JSON.parse(row.data), expires: row.expires }
          : null;
      },
      delete: async (token) => {
        db.prepare("DELETE FROM challenges WHERE token = ?").run(token);
      },
      listExpired: async () => {
        const rows = db
          .prepare("SELECT token FROM challenges WHERE expires <= ?")
          .all(Date.now());

        return rows.map((row) => row.token);
      },
    },
    tokens: {
      store: async (tokenKey, expires) => {
        db.prepare(
          "INSERT OR REPLACE INTO tokens (key, expires) VALUES (?, ?)"
        ).run(tokenKey, expires);
      },
      get: async (tokenKey) => {
        const row = db
          .prepare("SELECT expires FROM tokens WHERE key = ? AND expires > ?")
          .get(tokenKey, Date.now());

        return row ? row.expires : null;
      },
      delete: async (tokenKey) => {
        db.prepare("DELETE FROM tokens WHERE key = ?").run(tokenKey);
      },
      listExpired: async () => {
        const rows = db
          .prepare("SELECT key FROM tokens WHERE expires <= ?")
          .all(Date.now());

        return rows.map((row) => row.key);
      },
    },
  },
});

export default cap;

Now, you can connect this to your backend to expose the routes needed for the widget:

js
import { Elysia } from "elysia";
import cap from "...";

new Elysia()
  .post("/cap/challenge", async () => {
    return await cap.createChallenge();
  })
  .post("/cap/redeem", async ({ body, set }) => {
    const { token, solutions } = body;
    if (!token || !solutions) {
      set.status = 400;
      return { success: false };
    }
    return await cap.redeemChallenge({ token, solutions });
  })
  .listen(3000);
js
import express from "express";
import cap from "...";

const app = express();
app.use(express.json());

app.post("/cap/challenge", async (req, res) => {
  res.json(await cap.createChallenge());
});

app.post("/cap/redeem", async (req, res) => {
  const { token, solutions } = req.body;
  if (!token || !solutions) {
    return res.status(400).json({ success: false });
  }
  res.json(await cap.redeemChallenge({ token, solutions }));
});

app.listen(3000);
js
import Fastify from "fastify";
import cap from "...";

const fastify = Fastify();

fastify.post("/cap/challenge", async (req, res) => {
  res.send(await cap.createChallenge());
});

fastify.post("/cap/redeem", async (req, res) => {
  const { token, solutions } = req.body;
  if (!token || !solutions) {
    return res.code(400).send({ success: false });
  }
  res.send(await cap.redeemChallenge({ token, solutions }));
});

fastify.listen({ port: 3000 });

In this example, the Cap API is at /cap/ — set that in your widget as data-cap-api-endpoint (see widget docs).

When someone completes the CAPTCHA and sends the token back to your backend, you can validate the token and proceed with your logic.

js
const { success } = await cap.validateToken("...");

if (!success) throw new Error("invalid cap token");

// ...your logic

Methods and arguments

new Cap({ ... })

Arguments

json
{
  // used for json keyval storage. storage hooks are recommended instead
  "tokens_store_path": ".data/tokensList.json",

  // disables all filesystem operations, usually used along editing the state. storage hooks are recommended instead
  "noFSState": false,

  "disableAutoCleanup": false,

  "storage": {
    "challenges": {
      "store": "async (token, challengeData) => {}",
      "read": "async (token) => {}",
      "delete": "async (token) => {}",
      "listExpired": "async () => []"
    },
    "tokens": {
      "store": "async (tokenKey, expires) => {}",
      "get": "async (tokenKey) => {}",
      "delete": "async (tokenKey) => {}",
      "listExpired": "async () => []"
    }
  },

  "state": {
    "challengesList": {},
    "tokensList": {}
  }
}

You can always access or set the options of the Cap class by accessing or modifying the cap.config object.

await cap.createChallenge({ ... })

Arguments

json
{
  "challengeCount": 50,
  "challengeSize": 32,
  "challengeDifficulty": 4,
  "expiresMs": 600000
}

Response: { challenge, token, expires }

cap.redeemChallenge({ ... })

json
{
  token,
  solutions
}

Response: { success, token }

await cap.validateToken("...", { ... })

Arguments:

json
{
  "keepToken": false
}

Response: { success }

await cap.cleanup()

Cleans up all expired challenges and tokens. This is usually done for you by default.

Built in Europe 🇪🇺
Released under the Apache 2.0 License.