npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bhaskardey772/captcha

v1.0.0

Published

SVG CAPTCHA generator with HMAC token verification. Zero runtime dependencies.

Readme

captcha

SVG CAPTCHA generator with HMAC token verification — zero runtime dependencies.

npm version license node

Generates distorted SVG text CAPTCHAs on your Node.js server and verifies answers using stateless HMAC-signed tokens. No sessions, no databases, no native addons.

Architecture: capta runs entirely on the backend. Your server generates { svg, token } and sends both to the browser. The browser displays the SVG and submits the typed answer + token back. Your server verifies — done.


Install

npm install @bhaskardey772/captcha

React + Express integration

This is the most common setup: a React frontend protected by an Express backend.

1. Backend (Express)

// server.js
const express = require('express');
const capta   = require('@bhaskardey772/captcha');

const app = express();
app.use(express.json());

// Create once at startup — bakes in your secret and defaults
const captcha = capta.configure({
  secret:     process.env.CAPTCHA_SECRET, // keep server-side only
  ttl:        300,        // 5-minute expiry
  length:     5,
  distortion: 'medium',
});

// GET /api/captcha — send fresh SVG + token to the browser
app.get('/api/captcha', (req, res) => {
  res.json(captcha.create());
});

// POST /api/register — verify before processing
app.post('/api/register', (req, res) => {
  const { captchaToken, captchaAnswer, ...userData } = req.body;

  const result = captcha.verify(captchaToken, captchaAnswer);
  if (!result.valid) {
    // reason: 'wrong_answer' | 'expired' | 'sig_mismatch' | 'malformed'
    return res.status(422).json({ error: 'CAPTCHA failed', reason: result.reason });
  }

  // Bot check passed — create the user
  res.json({ success: true });
});

2. Frontend — <CaptchaField> component (React)

// CaptchaField.jsx
import { useState, useEffect, useCallback } from 'react';

export function CaptchaField({ onChange }) {
  const [svg,   setSvg]   = useState('');
  const [token, setToken] = useState('');

  const refresh = useCallback(async () => {
    const data = await fetch('/api/captcha').then(r => r.json());
    setSvg(data.svg);
    setToken(data.token);
    onChange({ token: data.token, answer: '' });
  }, [onChange]);

  useEffect(() => { refresh(); }, [refresh]);

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
      {/* Render the SVG inline — no <img> or data URL needed */}
      <div dangerouslySetInnerHTML={{ __html: svg }} />

      <button type="button" onClick={refresh} aria-label="Refresh CAPTCHA">
        ↺
      </button>

      <input
        type="text"
        placeholder="Type the characters above"
        autoComplete="off"
        onChange={e => onChange({ token, answer: e.target.value })}
      />
    </div>
  );
}

3. Wiring it into a form

// RegisterForm.jsx
import { useState } from 'react';
import { CaptchaField } from './CaptchaField';

export function RegisterForm() {
  const [captcha, setCaptcha] = useState({ token: '', answer: '' });
  const [error,   setError]   = useState('');

  async function handleSubmit(e) {
    e.preventDefault();
    setError('');

    const res = await fetch('/api/register', {
      method:  'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email:         e.target.email.value,
        password:      e.target.password.value,
        captchaToken:  captcha.token,
        captchaAnswer: captcha.answer,
      }),
    });

    if (!res.ok) {
      const { reason } = await res.json();
      setError(reason === 'expired' ? 'CAPTCHA expired — please refresh.' : 'Wrong answer, try again.');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email"    type="email"    placeholder="Email"    required />
      <input name="password" type="password" placeholder="Password" required />

      <CaptchaField onChange={setCaptcha} />
      {error && <p style={{ color: 'red' }}>{error}</p>}

      <button type="submit">Register</button>
    </form>
  );
}

Quick start (minimal)

const capta = require('@bhaskardey772/captcha');

// Generate
const { svg, token } = capta.create({ secret: process.env.CAPTCHA_SECRET });
// → svg:   '<svg ...>...</svg>'  send to browser for display
// → token: 'eyJ0Ij...'          send as hidden field or JSON

// Verify (on form submit)
const result = capta.verify(token, userTypedAnswer, process.env.CAPTCHA_SECRET);
// → { valid: true,  reason: 'ok' }
// → { valid: false, reason: 'wrong_answer' | 'expired' | 'sig_mismatch' | 'malformed' }

API

capta.create(options){ svg, token }

Generates random text, builds a distorted SVG, and returns a signed HMAC token.

| Option | Type | Default | Description | |--------|------|---------|-------------| | secret | string | required | HMAC signing key (server-side only) | | length | number | 6 | Number of characters | | ttl | number | 300 | Token expiry in seconds | | width | number | 200 | SVG width (px) | | height | number | 70 | SVG height (px) | | fontSize | number | 36 | Base font size (px) | | background | string | '#f0f0f0' | SVG background color | | color | string | '#333333' | Text color | | charset | string | unambiguous alphanumeric | Characters to draw from | | noise | boolean | true | Add noise lines and dots | | distortion | 'low'\|'medium'\|'high' | 'medium' | Distortion intensity |

capta.verify(token, answer, secret){ valid, reason }

Verifies a token against the user's typed answer. Stateless — no session or database needed.

Verification is case-insensitive and trims whitespace automatically.

| reason | Meaning | |----------|---------| | 'ok' | Correct answer, token valid | | 'wrong_answer' | Signature valid but answer is incorrect | | 'expired' | Token TTL has elapsed | | 'sig_mismatch' | Token was tampered with or signed with a different secret | | 'malformed' | Not a valid capta token |

capta.configure(options){ create, verify }

Returns a reusable instance with secret and defaults baked in — avoids repeating options on every call.

const captcha = capta.configure({ secret: process.env.CAPTCHA_SECRET, ttl: 600 });

const { svg, token } = captcha.create();                  // no secret needed here
const result          = captcha.verify(token, userAnswer); // no secret needed here

capta.createSvg(text, options)string

Low-level: build an SVG from arbitrary text without creating a token. Useful when you store the answer yourself (session, Redis, etc.).

const svg = capta.createSvg('AB3K7', { width: 200, height: 70 });
req.session.captchaAnswer = 'ab3k7';

capta.createToken(text, secret, options)string

Low-level: sign text into an HMAC token without generating an SVG.


How it stops bots

| Attack vector | Protection | |---------------|-----------| | Simple form bots | Must visually solve the distorted SVG challenge | | Automated token replay | Token expires after ttl seconds; combine with a Redis nonce denylist for strict one-time-use | | Token forgery | HMAC-SHA256 signed with a server-only secret — impossible to fake without the key | | Answer brute-force | Default 5-char charset = 54^5 ≈ 459 million combinations; pair with rate-limiting for extra protection | | Token tampering | Payload verified with crypto.timingSafeEqual before decoding — any modification is detected | | OCR bots | Per-character rotation, skew, jitter + SVG turbulence filter + noise lines/dots break optical recognition |


How it works

SVG distortion — three independent layers rendered by the browser:

  1. Per-character transforms — each character gets its own random rotate (±18°), skewX (±12°), vertical jitter (±8 px), and font-size variation (±18%)
  2. SVG filterfeTurbulence + feDisplacementMap applied to the text group; a random seed ensures every CAPTCHA looks unique
  3. Structural noise — wavy <path> lines and dot scatter <circle> elements overlaid on top of the text

Token format: BASE64URL(JSON_PAYLOAD).BASE64URL(HMAC_SHA256)

The payload contains the normalized answer, expiry timestamp, and a random nonce. The HMAC signature is verified with crypto.timingSafeEqual before the payload is decoded, preventing timing attacks.


Security notes

  • Store CAPTCHA_SECRET in an environment variable — never hardcode it or expose it to the client
  • Generate a strong secret: openssl rand -hex 32
  • For strict one-time-use enforcement, maintain a short-lived Redis SET of used nonces (the n field in the decoded payload) matching the token ttl
  • The default charset excludes visually ambiguous characters (0/O, 1/I/l) to avoid frustrating real users
  • capta does not provide rate-limiting — add that at the API layer (e.g., express-rate-limit)

License

MIT