capcode
v0.0.5
Published
Zero-dependency Node.js CAPTCHA generator - PNG/SVG/JPEG image with HMAC-SHA256 signed token & TTL. Stateless pure TypeScript alternative to svg-captcha and captcha-canvas.
Maintainers
Readme
capcode - Node.js CAPTCHA Generator (PNG / SVG / JPEG)
Zero-dependency Node.js CAPTCHA library with HMAC-SHA256 signed tokens & TTL. Stateless, pure TypeScript alternative to
svg-captchaandcaptcha-canvas— no native dependencies, no Redis/session required.
Generates a random code → renders it as a PNG / SVG / JPEG image → produces a signed, opaque token (HMAC-SHA256) → verifies user input against the token. The token cannot be reversed without the secret. Tokens carry a timestamp and support expiry (maxAge). Perfect for Express.js, Fastify, Next.js and any Node.js framework.
Zero dependencies. Uses only Node.js built-ins (crypto, zlib). All formats (PNG, SVG, JPEG) are generated in pure TypeScript. By Cihat Kösem.

Install
npm install capcodeUsage
import { createCaptcha, verifyCode } from "capcode";
// 1) Create a captcha
const captcha = createCaptcha({ secret: process.env.CAPTCHA_SECRET });
captcha.id; // signed opaque token `nonce.ts.tag` (code cannot be recovered without the secret)
captcha.code; // plaintext code (KEEP SERVER-SIDE ONLY)
captcha.image.buffer; // image Buffer (PNG by default)
captcha.image.base64; // base64 string
captcha.image.dataUrl; // data URL for <img src="...">
captcha.image.blob; // Web Blob
captcha.image.mimeType; // "image/png" | "image/svg+xml" | "image/jpeg"
captcha.image.width; // rendered width (px)
captcha.image.height; // rendered height (px)
// 2) Send id + image to the client, NEVER the code
// (e.g. Express: res.json({ id: captcha.id, image: captcha.image.dataUrl }))
// 3) When the user submits, verify
const ok = verifyCode(captcha.id, userInput, { secret: process.env.CAPTCHA_SECRET });
// ok === true → correct codeToken expiry (maxAge)
// Produce a token that carries a timestamp (always embedded in v2 format)
const captcha = createCaptcha({ secret: process.env.CAPTCHA_SECRET });
// Verify and reject if more than 5 minutes (300 s) have passed
const ok = verifyCode(captcha.id, userInput, {
secret: process.env.CAPTCHA_SECRET,
maxAge: 300,
});Tokens issued without maxAge never expire automatically — you control the policy at verify time.
Important
- A
secretis required. The id is a signed token produced with HMAC-SHA256; all three functions (createCaptcha,hashCode,verifyCode) throw without it. - The token is opaque: it leaks neither the code nor its length, and a random
noncemakes every token unique even for identical codes. verifyCodeuses a timing-safe comparison.- Input is normalized:
" a b 3x "→"AB3X"(whitespace stripped, upper-cased). - Keep the
secretserver-side only. Never ship it to the client.
Why capcode? (Comparison with Alternatives)
Unlike most CAPTCHA libraries that only render graphics and force you to manage session state or Redis stores, capcode handles the entire authentication lifecycle statelessly while maintaining zero external dependencies.
| Feature / Capability | capcode | svg-captcha | captcha-canvas | trek-captcha |
|:---|:---:|:---:|:---:|:---:|
| Zero Dependencies | ✅ Yes (0 deps) | ✅ Yes (0 deps) | ❌ No (requires node-canvas) | ❌ No (native binary) |
| No Native / C++ Builds | ✅ Pure JS/TS | ✅ Pure JS | ❌ Needs Cairo/Pango/Python | ❌ Native Rust binary |
| Output Formats | PNG, SVG, JPEG | SVG only | PNG, JPEG, SVG | PNG only |
| Built-in Stateless Verification | ✅ HMAC-SHA256/512 | ❌ (returns plain text) | ❌ (returns plain text) | ❌ (returns plain text) |
| Token Expiry (TTL / maxAge) | ✅ Built-in | ❌ (manual session/DB) | ❌ (manual session/DB) | ❌ (manual session/DB) |
| Timing-Safe Comparison | ✅ Built-in | ❌ None | ❌ None | ❌ None |
| Anti-OCR Geometric Distortion | ✅ Rotation, shear, scale jitter, overlap, chaotic waves | ⚠️ Basic curve & points | ⚠️ Basic distortion | ⚠️ Basic wave lines |
| Ready-to-use Output Helpers | ✅ buffer, base64, dataUrl, blob, mimeType | ❌ SVG text only | ⚠️ Buffer / stream | ⚠️ Buffer only |
| TypeScript Support | ✅ Native (included) | ⚠️ @types/svg-captcha | ✅ Native | ⚠️ @types/trek-captcha |
Anti-OCR hardening
Every image randomizes its geometry to defeat OCR/segmentation:
- Geometric deformation: per-character rotation, horizontal shear, and randomized width / height scaling.
- Overlapping characters: adjacent letters overlap, breaking segmentation.
- Noise layers: wavy background pattern, light lines behind text, darker lines over text, and salt-and-pepper dots.
- Dynamic jitter: wave line thickness varies per segment to prevent frequency analysis.
- Random letter spacing: gap between characters is randomized per image.
- Font variety: supply your own bitmap
glyphsto vary the typeface between deployments. Default charset excludes easily-confused characters (I,O,0,1).
API
createCaptcha(options?) → CaptchaResult
| Option | Type | Default | Description |
|----------------|-------------------|--------------------------------------|-------------|
| length | number | 6 | Code length (1–64) |
| charset | string | ABCDEFGHJKLMNPQRSTUVWXYZ23456789 | Allowed characters (must exist in glyphs) |
| secret | string | required | HMAC key — all three functions throw if omitted |
| scale | number | 8 | Base pixel scale (1–16) |
| algorithm | string | sha256 | Hash algorithm (sha512, ...) |
| difficulty | Difficulty | "medium" | easy / medium / hard |
| noise | NoiseOptions | — | Fine-grained overrides on top of the difficulty preset |
| glyphs | GlyphMap | built-in 5×7 bitmap font | Custom bitmap font |
| theme | ThemeOptions | — | textColor / backgroundColor / lineColor as [r,g,b] |
| format | "png" | "svg" | "jpeg" | "png" | Output image format |
| maxAge | number | — | Max token age in seconds — set at verify time, not here |
| renderImage | boolean | true | Set false to skip rendering (returns no image) |
Returned object: { id, code, image?: { buffer, base64, dataUrl, blob, mimeType, width, height } }
generateCode(options?) → string
Generates only a cryptographically random code.
hashCode(code, options?) → string
Normalizes a code and returns a signed opaque token nonce.ts.tag (requires secret). The ts field is a compact base-36 Unix timestamp enabling TTL verification.
verifyCode(id, code, options?) → boolean
Recomputes the token for the given code and the secret, then compares it to id in constant time. Optionally enforces maxAge (seconds). Returns false for malformed or expired tokens; throws if no secret is supplied.
Token format
v2 (current): nonce.ts.tag
v1 (legacy): nonce.tag ← still verified; rejected when maxAge is setnonce— 32 hex chars (16 random bytes), prevents precomputationts— Unix timestamp in base-36, compact and URL-safetag— HMAC(secret,nonce:ts:normalizedCode)
Output formats
| Format | mimeType | Notes |
|--------|------------------|-------|
| png | image/png | Default, lossless |
| svg | image/svg+xml | Scalable, smallest file size |
| jpeg | image/jpeg | Lossy, pure TypeScript — no extra deps |
createCaptcha({ secret, format: "svg" });
createCaptcha({ secret, format: "jpeg" }); // pure TS, no sharp neededVisual difficulty
| Difficulty | Dots density | Background lines | Foreground lines | Char overlap | Wavy pattern | Rotation | Shear |
|------------|--------------|------------------|------------------|--------------|--------------|----------|-------|
| easy | 0.5 | 1 | 0 | 0 | no | 0° | 0 |
| medium | 2.5 | 2 | 1 | 0 | no | 8° | 0.12 |
| hard | 6 | 4 | 3 | 1 (px) | yes | 20° | 0.3 |
Per-character width/height scaling is also randomized within ~±10–20%.
noise overrides example:
createCaptcha({
difficulty: "medium",
noise: { dots: 10, foregroundLines: 5, rotation: 25, shear: 0.4, widthScale: [0.8, 1.3] },
});Custom font
Provide a glyphs map where each value is an array of 7 numbers, each a 5-bit row (MSB = leftmost pixel):
import { createCaptcha, DEFAULT_GLYPHS } from "capcode";
const glyphs = { ...DEFAULT_GLYPHS, "#": [0x04, 0x04, 0x0a, 0x04, 0x11, 0x11, 0x11] };
createCaptcha({ charset: "A#2", glyphs, secret: "s" });Theme / colors
The theme option controls the rendered colors as [r, g, b] (0–255):
createCaptcha({
theme: {
textColor: [40, 120, 200], // exact color used for the glyphs
backgroundColor: [245, 240, 230], // canvas background
lineColor: [120, 90, 200], // overrides noise-line color (optional)
},
secret: "s",
});textColor— when omitted, each character is drawn in a random dark shade to resist OCR.backgroundColor— canvas background (default white).lineColor— if set, both background and foreground noise lines use this color; otherwise lines are auto-picked to contrast with the background.
Colors outside the 0–255 range throw a RangeError.
Test
npm testBenchmark
npm run benchmarkMeasures captcha rendering (easy / medium / hard, PNG and SVG) and token operations (hashCode / verifyCode).
Security
Audited:
0.0.2— 2026-08-29. Zero dependencies, no native build, HMAC-SHA256 opaque tokens with TTL andtimingSafeEqual. Full report:SECURITY.md.
Quick hardening:
// Always enforce TTL at verify time
verifyCode(id, userInput, { secret: SECRET, maxAge: 300, algorithm: "sha256" });
// Keep SECRET ≥32 bytes server-side only, pin "capcode": "0.0.2" exactlyOne low-severity note: future timestamps (ts > now + 60s) should be rejected — see SECURITY.md#3.2 for wrapper fix until upstream patch. No backdoor, npm audit 0 vulns.
Report issues at https://github.com/cihatksm/capcode/issues.
FAQ
How to add CAPTCHA to Express.js / Node.js?
Use createCaptcha({ secret }) to generate id + image.dataUrl, send both to the client, then verify with verifyCode(id, userInput, { secret, maxAge: 300 }). No session or Redis needed — see Usage.
capcode vs svg-captcha vs captcha-canvas?
Unlike svg-captcha (SVG only, plain text) and captcha-canvas (requires node-canvas native build), capcode supports PNG, SVG and JPEG in pure TypeScript, with built-in HMAC-SHA256 stateless verification, TTL and timing-safe comparison. See Why capcode?.
Does capcode support JPEG and SVG?
Yes. createCaptcha({ secret, format: "svg" }) or format: "jpeg" — both generated without extra dependencies. See Output formats.
Is capcode secure for production?
Yes. Audited 0.0.2 (2026-08-29), zero dependencies, timingSafeEqual, opaque nonce.ts.tag tokens. See SECURITY.md.
License
MIT
