@cap.js/server
@cap.js/server
is Cap's server-side library for creating and validating challenges. Install it using your preferred package manager:
bun add @cap.js/server
npm i @cap.js/server
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:
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:
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);
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);
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.
const { success } = await cap.validateToken("...");
if (!success) throw new Error("invalid cap token");
// ...your logic
Methods and arguments
new Cap({ ... })
Arguments
{
// 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
{
"challengeCount": 50,
"challengeSize": 32,
"challengeDifficulty": 4,
"expiresMs": 600000
}
Response: { challenge, token, expires }
cap.redeemChallenge({ ... })
{
token,
solutions
}
Response: { success, token }
await cap.validateToken("...", { ... })
Arguments:
{
"keepToken": false
}
Response: { success }
await cap.cleanup()
Cleans up all expired challenges and tokens. This is usually done for you by default.