264 lines
5.8 KiB
Markdown
264 lines
5.8 KiB
Markdown
# @cap.js/server
|
|
|
|
`@cap.js/server` is Cap's server-side library for creating and validating challenges. Install it using your preferred package manager:
|
|
|
|
::: code-group
|
|
|
|
```bash [bun]
|
|
bun add @cap.js/server
|
|
```
|
|
|
|
```bash [npm]
|
|
npm i @cap.js/server
|
|
```
|
|
|
|
```bash [pnpm]
|
|
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:
|
|
|
|
::: code-group
|
|
|
|
```js [Elysia]
|
|
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 [Express]
|
|
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 [Fastify]
|
|
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](./widget.md)).
|
|
|
|
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.
|