@007captcha/server
v0.1.0
Published
007captcha server-side token verification
Readme
Handles everything security-sensitive: challenge generation, behavioral analysis, scoring, and HMAC-SHA256 token signing. The client widget acts as a thin rendering layer — all verification logic runs here.
Zero runtime dependencies. Uses only Node.js built-in crypto.
Installation
pnpm add @007captcha/serverToken Verification
Verify signed tokens from any challenge method:
import { verify } from '@007captcha/server';
const result = await verify(token, SECRET);
if (result.success) {
// Allow the request
}verify(token, secretKey): Promise<VerifyResult>
| Field | Type | Description |
|-------|------|-------------|
| success | boolean | true if valid signature and not flagged as bot |
| score | number | 0.0 (bot) to 1.0 (human) |
| method | string | 'shape', 'maze', or 'ball' |
| challenge | string | Specific challenge identifier |
| verdict | string | 'human', 'uncertain', or 'bot' |
| timestamp | number | When the challenge was completed |
| error | string? | Reason if verification failed |
Tokens are single-use and expire after 5 minutes.
Challenge Managers
Each challenge method has a session manager that handles the full lifecycle: session creation, challenge delivery, input analysis, and token signing. Create one instance per server process.
Ball — BallChallengeManager
The ball challenge generates a physics-based trajectory in real-time and streams rendered frames to the client via SSE. After the challenge, the user's cursor path is analyzed against the recorded trajectory.
import { BallChallengeManager } from '@007captcha/server';
const ball = new BallChallengeManager(SECRET);Endpoints required:
| Method | Path | Handler |
|--------|------|---------|
| POST | /captcha/ball/start | ball.createSession() |
| GET | /captcha/ball/:id/stream | ball.startStreaming(id, onFrame, onEnd) |
| POST | /captcha/ball/:id/verify | ball.verify(id, points, cursorStartT, origin) |
Maze — MazeChallengeManager
Generates a procedural maze, renders it as a PNG, and solves it server-side. The client receives only the image and zone coordinates. Cursor path analysis runs entirely on the server.
import { MazeChallengeManager } from '@007captcha/server';
const maze = new MazeChallengeManager(SECRET);Endpoints required:
| Method | Path | Handler |
|--------|------|---------|
| POST | /captcha/maze/start | maze.createSession() |
| POST | /captcha/maze/:id/verify | maze.verify(id, points, origin) |
Shape — ShapeChallengeManager
Assigns a random shape (circle, triangle, or square) and analyzes the user's drawing server-side. The client only knows which shape to draw — scoring and detection run here.
import { ShapeChallengeManager } from '@007captcha/server';
const shape = new ShapeChallengeManager(SECRET);Endpoints required:
| Method | Path | Handler |
|--------|------|---------|
| POST | /captcha/shape/start | shape.createSession() |
| POST | /captcha/shape/:id/verify | shape.verify(id, points, origin) |
Express Integration
Full working example with all three methods:
import express from 'express';
import {
verify,
BallChallengeManager,
MazeChallengeManager,
ShapeChallengeManager,
} from '@007captcha/server';
const app = express();
const SECRET = process.env.CAPTCHA_SECRET;
const ball = new BallChallengeManager(SECRET);
const maze = new MazeChallengeManager(SECRET);
const shape = new ShapeChallengeManager(SECRET);
app.use(express.json());
// Ball
app.post('/captcha/ball/start', (req, res) => {
res.json(ball.createSession());
});
app.get('/captcha/ball/:id/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
let done = false;
const ok = ball.startStreaming(
req.params.id,
(frame) => res.write(`event: frame\ndata: ${JSON.stringify(frame)}\n\n`),
() => { done = true; res.write('event: end\ndata: {}\n\n'); res.end(); },
);
if (!ok) { res.end(); return; }
req.on('close', () => { if (!done) ball.cancelSession(req.params.id); });
});
app.post('/captcha/ball/:id/verify', (req, res) => {
const { points, cursorStartT, origin } = req.body;
res.json(ball.verify(req.params.id, points || [], cursorStartT || 0, origin || ''));
});
// Maze
app.post('/captcha/maze/start', (req, res) => res.json(maze.createSession()));
app.post('/captcha/maze/:id/verify', (req, res) => {
res.json(maze.verify(req.params.id, req.body.points || [], req.body.origin || ''));
});
// Shape
app.post('/captcha/shape/start', (req, res) => res.json(shape.createSession()));
app.post('/captcha/shape/:id/verify', (req, res) => {
res.json(shape.verify(req.params.id, req.body.points || [], req.body.origin || ''));
});
// Token verification
app.post('/verify', async (req, res) => {
res.json(await verify(req.body.token || '', SECRET));
});
app.listen(3007);See examples/express-server/ for the full demo with a UI.
Session Lifecycle
- Sessions are stored in memory and auto-expire (60s for ball/shape, 120s for maze).
- Each session can only be verified once.
- Call
manager.destroy()on server shutdown to clean up timers. - For horizontally scaled deployments, sessions are per-process — route challenge requests to the same instance (sticky sessions or a shared store).
