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

@dxv-systems/turnstile

v0.3.1

Published

Cloudflare Turnstile for DXV apps — siteverify core, Express and Next.js route guards, and the React widget

Downloads

1,764

Readme

@dxv-systems/turnstile

Cloudflare Turnstile for DXV apps: the /siteverify core, route guards for Express and Next.js App Router, and the React widget.

Extracted from dxv-platform, where it guards the portal's unauthenticated auth routes.

npm i @dxv-systems/turnstile

Server

Both guards take the same two options and share one policy implementation, so they cannot drift apart.

| Option | Meaning | |---|---| | secret | Usually process.env.TURNSTILE_SECRET_KEY. undefined is a legitimate state outside production. | | failClosed | What an absent secret means here. true → deny with 503 rather than serve unguarded. false → no secret, no guard. |

Express

import { requireTurnstile } from "@dxv-systems/turnstile/express";

app.post("/api/login", requireTurnstile({
  secret: process.env.TURNSTILE_SECRET_KEY,
  failClosed: isGuardedDeployment(),
}), handler);

Next.js App Router

import { assertTurnstile } from "@dxv-systems/turnstile/next";

export async function POST(req: NextRequest) {
  const denied = await assertTurnstile(req, {
    secret: process.env.TURNSTILE_SECRET_KEY,
    failClosed: isGuardedDeployment(),
  });
  if (denied) return denied;

  const body = Body.parse(await req.json());
  // ...
}

assertTurnstile takes a plain web Request and returns a plain Response — nothing here imports Next, so it works in any fetch-style handler. It never reads the body, so the handler's own parse still works.

What denies

Every axis fails closed, with one deliberate exception.

| Situation | Result | |---|---| | No secret, failClosed: false | allowed — the explicit "no guard here" state | | No secret, failClosed: true | 503, and an error log | | No token on the request | 403, without calling /siteverify | | Challenge rejected | 403, codes logged | | /siteverify unreachable, times out, or 5xxs | 503 — an outage must not become a bypass | | Secret wrong (invalid-input-secret and friends) | 503, not 403 — this is our deploy being wrong, not the visitor being a bot |

failClosed governs an absent secret only. Once a secret exists, a missing token and an unavailable check both deny regardless.

The client address

remoteip is taken from x-real-ip only — the header Vercel's own ipAddress() helper reads. Never x-forwarded-for: a caller can send its own, and proxies append rather than replace, so the leftmost entry is caller-controlled.

There is deliberately no fallback. Cloudflare scores the token against remoteip, so a forged or wrong value poisons the scoring — worse than sending none, which is what happens when the header is absent.

Why no environment sniffing

This package never reads the environment. How an app recognises its own production deployment differs per host and per repo, and baking one answer in here would export it to every consumer. Callers pass failClosed and keep that decision where it belongs.

A worked example of isGuardedDeployment, and the two traps behind it:

// NOT NODE_ENV: vercel.json commonly pins it to "production" for every target,
// preview included. NOT VERCEL_ENV either: a Vercel *custom* environment
// reports "preview", so staging is invisible to it — and staging usually does
// have a real widget and secret, so it must fail closed too.
export function isGuardedDeployment(): boolean {
  const env = process.env.VERCEL_TARGET_ENV;
  return env === "production" || env === "staging";
}

VERCEL_TARGET_ENV names custom environments and requires expose_system_env_vars: true on the project. Where it is unset — local dev, or a host that is not Vercel — the guard falls open, which is the right outcome for an environment with no widget provisioned.

Client

import { Turnstile, turnstileHeaders, type TurnstileHandle } from "@dxv-systems/turnstile/react";

const [token, setToken] = useState<string | null>(null);
const widget = useRef<TurnstileHandle>(null);

async function onSubmit() {
  try {
    await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json", ...turnstileHeaders(token) },
      body: JSON.stringify({ email, password }),
    });
  } finally {
    widget.current?.reset(); // tokens are single-use
  }
}

<Turnstile ref={widget} siteKey={SITE_KEY} failedMessage="Verification could not load." onToken={setToken} />
<button type="submit" disabled={!token}>Sign in</button>

siteKey and failedMessage are props, not read from the environment: the public-var prefix differs per bundler (VITE_*, NEXT_PUBLIC_*), and consuming apps disagree about whether they have an i18n runtime.

Theme

theme defaults to "light", deliberately not Cloudflare's own "auto". auto follows the operating system's prefers-color-scheme, which has nothing to do with the host app's theme — so a light-only app renders a black widget for every visitor whose OS is in dark mode, which is what happened across the DXV fleet before 0.2.0.

Pass "auto" explicitly if your app genuinely follows the system scheme, or "dark" if it is dark-only. An app with its own theme toggle should pass its current theme, so the widget re-renders when it changes.

Where there is no real widget — local dev, preview deployments whose randomised hostnames can never match the domain allowlist — fall back to TURNSTILE_TEST_SITE_KEY, Cloudflare's always-passes key. That cannot weaken production: the sitekey is public, the server is the gate, and a token minted against it fails /siteverify against a real secret.

Wrap it in your app

Consume ./react through a thin app-owned component rather than importing it into pages directly:

// components/turnstile.tsx
"use client"; // Next only

import { Turnstile as Base, TURNSTILE_TEST_SITE_KEY, type TurnstileHandle } from "@dxv-systems/turnstile/react";

export const Turnstile = forwardRef<TurnstileHandle, { onToken: (t: string | null) => void }>(
  function Turnstile(props, ref) {
    return (
      <Base
        ref={ref}
        siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || TURNSTILE_TEST_SITE_KEY}
        failedMessage={t("turnstile.failed")}
        {...props}
      />
    );
  },
);

The wrapper is where the sitekey and the copy come from, and in Next it is also the "use client" boundary. The package's own "use client" directive survives the build but lands after the "use strict" that the CommonJS emit adds, so do not rely on it to make the boundary for you.

Development

npm run build --workspace @dxv-systems/turnstile
npm run test  --workspace @dxv-systems/turnstile

src/shared.ts holds the pieces both halves need (TURNSTILE_HEADER, TURNSTILE_TEST_SITE_KEY, turnstileHeaders). Keep it that way: if ./react imports them from ./index instead, the whole server policy — /siteverify, the error-code table, evaluateTurnstile — lands in consumers' browser bundles, because CommonJS output defeats tree-shaking.

Output is CommonJS — Vercel's serverless runtime bundles to CJS, and an ESM-only dependency there is a runtime ERR_REQUIRE_ESM, not a build error.