rotaptcha-node
v2.1.1
Published
A modern, gamified CAPTCHA solution for Node.js
Readme
rotaptcha-node

A modern, gamified CAPTCHA solution for Node.js — no distorted text, no annoyance, just fun interactive challenges that users actually enjoy.
Features
- Rotation-based puzzle – identify the rotation angle of shapes in a circular viewport
- Zero external dependencies – uses only Canvas API for shape rendering
- Framework-agnostic – works seamlessly with Express, Fastify, Next.js API routes, Hono, Elysia, and any Node.js server
- Simple UUID-based verification – generate challenge, verify answer in one line
- Blazing fast – sub-millisecond verification, in-memory storage with LokiJS
- Fully customizable – adjust canvas size, rotation range, wobble effects, noise, and stroke width
Installation
npm install rotaptcha-nodeyarn add rotaptcha-nodepnpm add rotaptcha-nodebun add rotaptcha-nodeHow It Works
rotaptcha generates a visual puzzle where:
- Four random shapes (circles, squares, triangles, pentagons, hexagons) are drawn in quadrants
- The center circular area shows the same shapes but rotated by a specific angle
- The user must identify the rotation angle to solve the challenge
The rotation angle is stored server-side with a unique UUID, and verification happens by comparing the user's answer.
Quick Start
Backend Example (Express)
import express from 'express';
import rotaptcha from 'rotaptcha-node';
const app = express();
app.use(express.json());
// Your secret key for JWT encryption (keep this secure!)
const SECRET_KEY = 'your-secret-key-min-32-chars-long';
// Generate a CAPTCHA challenge
app.get('/captcha/create', async (req, res) => {
const { image, token } = await rotaptcha.create({
width: 400,
height: 400,
minValue: 20,
maxValue: 90,
step: 10,
strokeWidth: 6,
wobbleIntensity: 3,
noise: true,
noiseDensity: 5,
expiryTime: 5 // 5 minutes
}, SECRET_KEY);
res.json({
image: `data:image/png;base64,${image}`,
token // Send token to frontend for verification
});
});
// Verify the user's answer
app.post('/captcha/verify', async (req, res) => {
const { token, answer } = req.body;
const isValid = await rotaptcha.verify({ token, answer }, SECRET_KEY);
if (!isValid) {
return res.status(400).json({ error: 'Invalid CAPTCHA or expired' });
}
res.json({ message: 'Success! CAPTCHA verified.' });
});
app.listen(3000, () => console.log('Server running on port 3000'));API Reference
rotaptcha.create(options, secretKey)
Generates a CAPTCHA image with rotated shapes in the center.
Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| options | CreateProps | - | Configuration object (see below) |
| secretKey | string | - | Required. Secret key for JWT encryption (minimum 32 characters) |
CreateProps Options
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| width | number | 400 | Canvas width in pixels |
| height | number | 400 | Canvas height in pixels |
| minValue | number | 20 | Minimum rotation angle in degrees |
| maxValue | number | 90 | Maximum rotation angle in degrees |
| step | number | 10 | Step size for rotation (e.g., 10 means 20, 30, 40...) |
| strokeWidth | number | 6 | Line thickness for drawing shapes |
| wobbleIntensity | number | 3 | Intensity of wobbly/hand-drawn effect (0 = none, higher = more wobble) |
| noise | boolean | true | Add visual noise (dots, lines, speckles) for anti-bot protection |
| noiseDensity | number | 5 | Density of noise elements (higher = more noise) |
| availableColors | string[] | See below | Array of RGB color strings for shapes |
| canvasBg | string | 'rgb(230, 230, 230)' | Canvas background color |
| expiryTime | number | 5 | Token expiry time in minutes |
Default colors: ['rgb(198, 231, 159)', 'rgb(230, 103, 171)', 'rgb(147, 128, 230)', 'rgb(255, 190, 152)', 'rgb(191, 230, 11)', 'rgb(88, 106, 175)', 'rgb(230, 122, 63)', 'rgb(223, 230, 73)']
Returns
Promise<{ image: string, token: string }>
image: Base64-encoded PNG imagetoken: Encrypted JWT token containing the challenge data
Implementation Details
The create method:
- Generates a random rotation angle using
randomWithStep(minValue, maxValue, step)- ensures the angle is a multiple of the step size (e.g., 20, 30, 40 if step=10) - Creates a unique UUID with
generateShortUuid()- an 8-character alphanumeric identifier - Creates an encrypted JWT token containing:
jti: Unique identifieranswer: The correct rotation angleiat: Issued at timestampexp: Expiry timestamp (current time + expiryTime minutes)
- Draws shapes using HTML5 Canvas:
- Four quadrants each get a random shape (circle, square, triangle, pentagon, or hexagon)
- Shapes are drawn with seeded randomness to ensure consistency
- A circular clipping mask is applied to the center
- The center area is redrawn with the same shapes rotated by the target angle
- Applies effects:
- Wobble: Makes shapes appear hand-drawn with curved edges (intensity controlled by
wobbleIntensity) - Noise: Adds dots, lines, crosses, and speckles for bot resistance (density controlled by
noiseDensity)
- Wobble: Makes shapes appear hand-drawn with curved edges (intensity controlled by
- Returns an object with
image(Base64 PNG string) andtoken(encrypted JWT)
Storage mechanism: Uses JWT tokens encrypted with your secret key. No server-side storage required - all challenge data is embedded in the token itself.
rotaptcha.verify(options, secretKey)
Verifies if the user's answer matches the stored rotation angle.
Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| options | VerifyProps | Yes | Verification object (see below) |
| secretKey | string | Yes | The same secret key used in create() |
VerifyProps Options
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| token | string | Yes | The JWT token returned from create() |
| answer | string | Yes | User's rotation angle guess (as string) |
Returns
Promise<boolean> - true if answer is correct and token not expired, false otherwise
Implementation Details
The verify method:
- Decrypts the JWT token using the secret key
- Checks token expiry - returns false if expired
- Parses the answer from string to integer using
parseInt() - Compares the answer with the token's stored rotation value
- Returns true if valid and not expired, false otherwise
Important: The verification is an exact match. The user must provide the precise rotation angle (e.g., "45" for 45 degrees).
Security features:
- Automatic expiration: Tokens expire after the configured time (default 5 minutes)
- No server storage: All data is encrypted in the token, preventing memory leaks
- JWT-based: Industry-standard token format with encryption
- Tamper-proof: Any modification to the token invalidates it
Additional recommendations:
- Use a strong secret key (minimum 32 characters)
- Implement rate limiting to prevent brute-force attacks
- Consider one-time use by tracking verified tokens in a short-lived cache
Advanced Usage
With Custom Styling
const SECRET_KEY = 'your-secret-key-min-32-chars-long';
const { image, token } = await rotaptcha.create({
width: 600,
height: 600,
minValue: 0,
maxValue: 180,
step: 10,
strokeWidth: 8,
wobbleIntensity: 5, // High wobble for hand-drawn aesthetic
noise: true,
noiseDensity: 8, // Extra noise for bot protection
availableColors: ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)'],
canvasBg: 'rgb(255, 255, 255)',
expiryTime: 10 // 10 minutes expiry
}, SECRET_KEY);With One-Time Verification (Replay Attack Prevention)
import Keyv from 'keyv';
const usedTokens = new Keyv({ ttl: 600000 }); // 10 minutes cache
const SECRET_KEY = 'your-secret-key-min-32-chars-long';
app.post('/captcha/verify', async (req, res) => {
const { token, answer } = req.body;
// Check if token was already used
if (await usedTokens.get(token)) {
return res.status(400).json({ error: 'Token already used' });
}
const isValid = await rotaptcha.verify({ token, answer }, SECRET_KEY);
if (isValid) {
// Mark token as used to prevent replay attacks
await usedTokens.set(token, true);
res.json({ success: true });
} else {
res.status(400).json({ error: 'Invalid CAPTCHA or expired' });
}
});Frontend Integration Example
<!DOCTYPE html>
<html>
<body>
<img id="captcha-image" />
<input type="number" id="rotation-input" placeholder="Enter rotation angle" />
<button onclick="verify()">Verify</button>
<script>
let currentToken = null;
// Load CAPTCHA on page load
async function loadCaptcha() {
const response = await fetch('/captcha/create');
const data = await response.json();
document.getElementById('captcha-image').src = data.image;
currentToken = data.token;
}
// Verify user's answer
async function verify() {
const answer = document.getElementById('rotation-input').value;
const response = await fetch('/captcha/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: currentToken, answer })
});
const result = await response.json();
alert(result.message || result.error);
// Load new CAPTCHA after verification attempt
if (result.success) {
loadCaptcha();
}
}
loadCaptcha();
</script>
</body>
</html>Architecture
Shape Generation
- Five shape types: circles, squares, triangles, pentagons, hexagons
- Seeded random function ensures consistency between initial draw and rotated draw
- Shapes are 85% of quadrant size by default for visual balance
Rotation Mechanism
- Draw shapes in four quadrants
- Apply circular clipping mask to center (radius = canvas width / 3)
- Clear the clipped area
- Apply rotation transformation
- Redraw the same shapes (using same seed)
- Remove clipping mask
Effects
- Wobble: Replaces straight lines with quadratic curves with random control points (intensity controlled by
wobbleIntensityparameter, default: 3) - Noise: Adds noise elements (dots, lines, crosses) and speckles with 40% opacity (density controlled by
noiseDensityparameter, default: 5)
Why rotaptcha?
Traditional CAPTCHAs frustrate users and hurt conversion rates. rotaptcha turns security into a delightful micro-interaction that's:
- Accessible – easier to solve than distorted text
- Fast – typical solve time under 5 seconds
- Effective – visual puzzles are harder for bots to automate
Start protecting your users today — no more "I can't read this squiggly text" support tickets.
License
MIT
Made with care for modern web applications.
