@broberg/forms-turnstile

中文文档
0.4.0发布于 2026/10/23MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 6584

为broberg.ai车队提供垃圾邮件保护的公有形式原始:蜂窝检测,一个在流程中的IP速率限制器,和Cloudflare Turnstile服务器侧校验. 无头核心 + 一个用于 React and Preact + a Hono 中间软件的部件钩.

中文使用文档
文档目录9 节
  1. 核心项目 @broberg/forms-turnstile 编号 : @broberg/forms-turnstile…
  2. "你是一个机器人","我们不能问"是不同的答案(v0.3.0)
  3. 本地 dev / CI ——不需要真正的密钥
  4. 运行时间现场钥匙交付
  5. 部件悬钩- 反应( S) /react )或预告( /preact (中文(简体) )
  6. 低键框——渲染选项(v. 4.0)
  7. 打开提交按钮 status ,没有打开 token (v.2.0) (中文(简体) )
  8. 反应笔记
  9. Hono 中间软件( @broberg/forms-turnstile/hono (中文(简体) )

@broberg/形式-翻转

垃圾邮件保护 公开形式 broberg.ai车队的原始:蜂窝检测,一个在流程中的IP速率限制器,和Cloudflare Turnstile服务器侧校验——外加Preact部件钩和Hono中件. 从 webhouse/cms(无头核心)与 xrt81'铅形式 (Preact/Hono e2e).

npm i @broberg/forms-turnstile      # exact-pin for prod-auth deps

核心项目@broberg/forms-turnstile 编号 : @broberg/forms-turnstile/server(中文(简体) )

框架不可知性, node:crypto 只有这个.

import { applySpamGauntlet, hashIp, getSitekeyResponse } from "@broberg/forms-turnstile/server";

const ipHash = hashIp(clientIp); // GDPR-friendly — never store the raw IP

const result = await applySpamGauntlet({
  honeypot: { body },                                          // omit to skip this layer
  rateLimit: { ipHash, formName: "contact", maxPerHour: 5 },    // omit to skip this layer
  turnstile: { token: body.token, secret: env.TURNSTILE_SECRET_KEY, remoteip: clientIp },
});
if (result.blocked) {
  // result.reason: "honeypot" | "rate-limit" | "turnstile"
}

每层都是 选入 ——只通过您想要的检查的选项密钥;它们按蜜壶的顺序运行故障快活 —— 速率限制 —— Turnstile.

个人支票也出口(isHoneypotTriggered, (中文(简体) ), isRateLimited, (中文(简体) ), validateTurnstile, (中文(简体) ), HONEYPOT_FIELD如果你宁愿自己叫他们.

"你是一个机器人","我们不能问"是不同的答案(v0.3.0)

verifyTurnstile() 返回时 3个 结果,因为可能发生三件事:

import { verifyTurnstile } from "@broberg/forms-turnstile/server";

const r = await verifyTurnstile(token, secret, { remoteip, timeoutMs: 10_000 });
// { ok: true }
// { ok: false, reason: "rejected",    errorCodes: [...] }   ← Cloudflare said no
// { ok: false, reason: "unavailable", detail: "…" }         ← we never got an answer

这个 从不扔 5xx、 HTML 错误页、 已丢弃的连接和 暂停 全部回来作为 unavailable 和可读的细节.

为什么会这样. 在v. 3. 0 之前 , 请检查url=值 (帮助), unavailable 联系到的呼叫者 两个 视云雀失败的形状而定不同的频道一个非JSON 机构 丢弃而JSON的尸体没有 success 返回字段 false。 。 。 . 没有一个呼叫者能够一致地处理它 false 树枝是有害的 一个 它变成了 reason: "turnstile",它使得 你辜负了机器人 检查"真实的人说她不是人类,"再试"是她唯一的 选择,她没有问题 日志上说 turnstile 同样,命名 错误的原因.

以出产为度量. 同样的缺陷 v. 2.0 固定在浏览器上 边 ——当客户端/服务器 双人有一个半个,先检查另一个半个.

没有判决不是判决.

选择政策

applySpamGauntlet 默认为 关闭失败数字 :

await applySpamGauntlet({ turnstile: { token, secret } });
// Cloudflare unreachable → THROWS. An unguarded route 500s. Nothing gets through.

这是蓄意的:它保留了这一一揽子方案已经为主导者所做的一切 所以升级从不会默默地把一个人的500元转换成400元 指责一个人类。 由你自己决定:

const r = await applySpamGauntlet({
  turnstile: { token, secret, onUnavailable: "block" },
});
if (r.blocked && r.reason === "turnstile-unavailable") {
  // Say what is true: "We can't check that right now — try shortly, or call us."
  // NOT "you failed the bot check".
}

turnstile-unavailable 独立成员 SpamBlockReason,所以一个 switch 在编译时会告诉你 你有一个新案子 手柄。 这两个选项也都放在Hono中间软件上(timeoutMs, (中文(简体) ), onUnavailable) (中文(简体) ).

现在暂停了 (违约10s). 没有了,一个被绞死的"云花" 连接将请求维持到平台停止引用为止——更糟糕的是 与本地相比,飞行/服务器无所适从,用户只看回旋器.

validateTurnstile() 已贬值但未作改变. 这是损失——它不能 分辨出两个失败, 并仍然把它们分成一个返回值和一个 掷出. 它的准确行为被测试所支撑 所以现有的呼叫者看不到任何变化 升级。 迁移到 verifyTurnstile 当你触摸呼叫站点.

费率限制提醒 : 仅限进程(a) Map——保护单机部署(飞来单机,一班机工人),但每个实例都有自己的计数器,所以它确实 没有 保护多层/无服务器。 对于共享的可插件存储限制器(Turso/Redis-backed),可访问范围为 @broberg/apikey是因为 SlidingWindowRateLimiter 换句话说.

测量您的实例数, 然后在常数旁边写 . 带 N 实例 maxPerHour × N没有 maxPerHour 所以说 数字在你的代码里是谎言 任何人阅读它下一个,除非评论说 所以说. flyctl scale show -a <app> 用一行来回答 测量的fd- sundhed 2 机器对 maxPerHour: 5 并将实际限值记录为10 地点 常数而不是在承诺的信息, 这是正确的地方.

这是重复的刹车 不是门 蜜壶和花心肠 保护; 如果利率限制是您所依赖的地层, 您需要一个 共享商店.

本地 dev / CI ——不需要真正的密钥

import { TURNSTILE_TEST_SITE_KEY, TURNSTILE_TEST_SECRET_KEY } from "@broberg/forms-turnstile/server";

云浮的官职 总是通过 测试密钥 – 安全可执行, 安全默认, 因此流的末端工作没有真正的 Turnstile 部件 .

不要用 E2E 装入 未解决 状态与测试键相对

他们解决了 几乎马上因此,支票喜欢 "提交按钮是 在用户解决挑战之前已禁用“ 正在比赛部件: 您正在尝试证明 存在在一秒钟.

fd-sundhed在收养时就打到这个了——同一说法在一页中传出 对方失败了, 不是因为应用程序行为不同 而是因为 种族主张。他们删除了支票,而不是增加一个等待,即 正确的呼唤:通过或失败的测试 都无法证明 向上转 这不是片面测试,而是状态测试 测试键不是 别动.

坚持坚持下去的各州—— solved 解决后, failed 与 联合国 error 当你屏蔽脚本。 注意这个陷阱 我们 已经建成, 总是通过键作为默认:方便和比赛 同样的特征.

运行时间现场钥匙交付

// GET /config route — serves the (public) site key at runtime so rotating it
// is a secret change, never a rebuild.
app.get("/config", (c) => c.json(getSitekeyResponse(env.TURNSTILE_SITE_KEY)));

部件悬钩- 反应( S)/react)或预告(/preact(中文(简体) )

懒惰地装入 Turnstile 脚本( cached + dedudeed) , 使部件一次成为 站点密钥可用. 两个适配器都是一样的 - 他们 钩子从哪一个包里取出不同,所以一个固定到达两个包.

import { useTurnstile } from "@broberg/forms-turnstile/react";   // or /preact

function ContactForm() {
  const { widgetRef, token, status, error, reset } = useTurnstile(siteKey);

  return (
    <form onSubmit={onSubmit}>
      {/* ...fields... */}
      <div ref={widgetRef} data-testid="contact-form-captcha" />
      <button type="submit" disabled={status !== "solved"}>Send</button>
      {status === "failed" && <p role="alert">Spam-tjekket kunne ikke indlæses. {error}</p>}
    </form>
  );
}

siteKey 也许 null编号 :undefined 运行时 /config 获取在 飞行——读作: loading。 。 。 .

低键框——渲染选项(v. 4.0)

useTurnstile(siteKey, { appearance: "interaction-only", theme: "light", size: "flexible" });

未经更改改为 turnstile.render数字 : appearance (单位:千美元)"always" | "execute" | "interaction-only", (中文(简体) ), theme (单位:千美元)"light" | "dark" | "auto", (中文(简体) ), size (单位:千美元)"normal" | "flexible" | "compact", (中文(简体) ), language与 "interaction-only" 仅框 当 Cloudflare 想要点击时出现; 否则 status 继续直走 改为 solved 无所可见. 选项无法替换站点密钥或 钩回召相. 没有选项, 部件就和0. 3. 0 完全一样.

打开提交按钮 status,没有打开 token (v.2.0) (中文(简体) )

status含义
loading没有站点密钥, 或脚本仍在装入中
ready部件已打开等待用户
solvedtoken 有效 —— 这是您唯一需要提交的国家
failed没有干预,它就无法运作; error 说为什么

为什么这很重要. 在v.2.0之前,钩子只暴露在 token空时 符号有两个原因: 用户尚未解析,以及 这永远不会 工作。表单标注在 !token 因此显示一个从未提交过的按钮 启用了, 没有任何地方说为什么。 转弯被普通挡住 隐私扩展往往足以使这是普通用户的经验,而不是 边框.

在同一沉默中结束的三种不同的道路——这个脚本未能 装入,一个脚本 已装入 时 window.turnstile 从来没有出现过 widgetRef 从来没有被附着。 三个现在都结束了 failed 带有 独立 error由fd-sundhed提起,他发现三个人中的第一个由 读取焦油丸.

reset() 返回已解开的部件到 ready它会 没有 移动 a failed 部件退出 failed 重置一个从未装入的部件无法修复, 和洗钱 进入一个充满希望的状态 会抹去唯一的证据 真正的问题.

反应笔记

react 是一个可选的对等点( e)>=18) (中文(简体) ). 捆绑带 "use client",所以一个 Next.js App路由器项目可以在不包含客户端组件的情况下从客户端组件导入它 标记任何额外的东西——只有反应包携带; /server 财务报告和审定财务报表 /hono 保持服务器安全.

Hono 中间软件(@broberg/forms-turnstile/hono(中文(简体) )

读取JSON体本身(检查蜂蜜壶场+Turnstile sorder),运行起伏装置,并使用短路 400 在街区。 顺便说一句 解剖后的尸体被藏在上下文上 spamCheckedBody 因此,你的处理器不重读(已经消耗)请求流.

import { honoTurnstileMiddleware } from "@broberg/forms-turnstile/hono";

app.post(
  "/api/contact",
  honoTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, formName: "contact", maxPerHour: 5 }),
  (c) => {
    const body = c.get("spamCheckedBody");
    // ...persist + notify...
    return c.json({ ok: true });
  },
);

版本与发布记录

@broberg/forms-turnstile 共发布 5 个版本,首次发布于 2026/07/02;过去 12 个月发布了 5 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.4.0latest2026/10/2316 天前
0.3.02026/08/232 个月前
0.2.12026/08/122 个月前
0.2.02026/08/122 个月前
0.1.02026/07/024 个月前

关于 @broberg/forms-turnstile 的常见问题

@broberg/forms-turnstile 是什么?

为broberg.ai车队提供垃圾邮件保护的公有形式原始:蜂窝检测,一个在流程中的IP速率限制器,和Cloudflare Turnstile服务器侧校验. 无头核心 + 一个用于 React and Preact + a Hono 中间软件的部件钩.

如何安装 @broberg/forms-turnstile?

在项目目录运行 npm install @broberg/forms-turnstile,或使用 pnpm add / yarn add / bun add。

@broberg/forms-turnstile 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@broberg/forms-turnstile 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@broberg/forms-turnstile 可以商用吗?

@broberg/forms-turnstile 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@broberg/forms-turnstile 的下载量有多少?

根据 npm 官方下载统计,@broberg/forms-turnstile 在 2026/10/09 – 2026/11/07 这 30 天内被下载 6,584 次,日均约 219 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@broberg/forms-turnstile 还在维护吗?

最新版本 0.4.0 发布于 2026/10/23(16 天前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/07/02。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。