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

@bliztek/turnstile

v1.0.0

Published

Headless, zero-dependency Cloudflare Turnstile integration for React

Readme

@bliztek/turnstile

Headless, zero-dependency Cloudflare Turnstile integration for React.

Installation

pnpm add @bliztek/turnstile

React is an optional peer dependency — only required for the client-side hook. The server-side verifyCaptcha function works without React.

Entry Points

| Import | What you get | |---|---| | @bliztek/turnstile | useTurnstile hook + client types | | @bliztek/turnstile/server | verifyCaptcha function + server types |

Client: useTurnstile

A React hook that handles script loading, widget rendering, token state, and cleanup.

"use client";

import { useTurnstile } from "@bliztek/turnstile";

function ContactForm() {
  const { ref, token, isReady, reset } = useTurnstile({
    siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!,
    theme: "dark",
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!token) return;

    const res = await fetch("/api/contact", {
      method: "POST",
      body: JSON.stringify({ turnstileToken: token, /* ...form data */ }),
    });

    if (res.ok) {
      reset(); // clears token and resets the widget
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
      <div ref={ref} className="min-h-[65px]" />
      <button type="submit" disabled={!token}>Submit</button>
    </form>
  );
}

Options

| Option | Type | Default | Description | |---|---|---|---| | siteKey | string | required | Your Cloudflare Turnstile site key | | theme | "light" \| "dark" \| "auto" | "auto" | Widget appearance | | size | "normal" \| "compact" | "normal" | Widget size | | appearance | "always" \| "execute" \| "interaction-only" | "always" | When to show the widget | | retry | "auto" \| "never" | "auto" | Retry behavior on failure | | retry-interval | number | 8000 | Retry interval in ms | | action | string | — | Action identifier for analytics | | cData | string | — | Custom data passed to verification response | | language | string | — | BCP 47 language code (e.g. "en", "de") | | tabindex | number | — | Tab index for accessibility | | onSuccess | (token: string) => void | — | Called when a token is obtained | | onExpire | () => void | — | Called when the token expires | | onError | (error: Error) => void | — | Called when the script fails to load |

Return Value

| Property | Type | Description | |---|---|---| | ref | RefCallback<HTMLElement> | Attach to the container element | | token | string \| null | Current token, or null | | isReady | boolean | Whether the Turnstile script has loaded | | reset | () => void | Reset the widget and clear the token |

How It Works

  1. On mount, the hook loads the Turnstile script (render=explicit mode) via a singleton promise — multiple hook instances share one script tag.
  2. When the container element mounts (via ref callback) and the script is ready, the widget renders automatically.
  3. On successful challenge completion, token updates and onSuccess fires.
  4. On unmount, the widget is removed to prevent memory leaks.
  5. SSR-safe — all browser APIs are guarded behind typeof window checks.
  6. If the script fails to load, the singleton resets so future attempts can retry.

Server: verifyCaptcha

Verifies a Turnstile token against the Cloudflare siteverify API. Uses only fetch — no SDK required.

import { verifyCaptcha } from "@bliztek/turnstile/server";

export async function POST(request: Request) {
  const { turnstileToken } = await request.json();

  const result = await verifyCaptcha({
    token: turnstileToken,
    secretKey: process.env.TURNSTILE_SECRET_KEY!,
    idempotencyKey: crypto.randomUUID(), // prevent replay attacks
  });

  if (!result.success) {
    return Response.json(
      { error: "Verification failed", details: result["error-codes"] },
      { status: 422 }
    );
  }

  // Token is valid — proceed with the request
}

Options

| Option | Type | Default | Description | |---|---|---|---| | token | string | required | The token from the client widget | | secretKey | string | required | Your Cloudflare Turnstile secret key | | endpoint | string | Cloudflare URL | Override for testing | | idempotencyKey | string | — | Prevents replay attacks (recommended) |

Error Handling

verifyCaptcha never throws. It always returns a TurnstileVerificationResponse:

  • Missing token or secretKey{ success: false, "error-codes": ["missing-input-response"] }
  • HTTP error from Cloudflare → { success: false, "error-codes": ["http-error-500"] }
  • Network failure → { success: false, "error-codes": ["network-error"] }

Response

interface TurnstileVerificationResponse {
  success: boolean;
  "error-codes"?: string[];
  challenge_ts?: string;
  hostname?: string;
  action?: string;
  cdata?: string;
}

Types

Client types (from @bliztek/turnstile):

  • UseTurnstileOptions — All hook options
  • UseTurnstileReturn — Hook return value
  • TurnstileRenderOptions — Widget render options (subset of UseTurnstileOptions)
  • TurnstileTheme, TurnstileSize, TurnstileAppearance, TurnstileRetry — Union types
  • TurnstileInstance, TurnstileWidgetOptions — Low-level Cloudflare API types

Server types (from @bliztek/turnstile/server):

  • VerifyCaptchaOptions
  • TurnstileVerificationResponse

Environment Variables

| Variable | Side | Description | |---|---|---| | NEXT_PUBLIC_TURNSTILE_SITE_KEY | Client | Public site key (safe to expose) | | TURNSTILE_SECRET_KEY | Server | Secret key (never expose to client) |

License

MIT