@ doman数字/合成
多曼数字客户端表格的被签名的合成提交协议. 数字
检查( E)dd-checks)通过客户的真实形式提交真正的询问
调度器; Turnstile 屏蔽合成浏览器, 所以处理器
接受一个已签名的跳过请求 只有转动的符号。 。 。 .
网络加密, 没有依赖性: 它运行在 Cloudflare Workers, Node 22+ 和 dd- checks 中的浏览器边签名助手。 两次测试 节点和工人.
协议在DOM-487上的"DD Checks: plan"文档中指定, 页:1 这个包是作为代码的这个部分.
安装
pnpm add @domandigital/synthetic
在窗体处理器中
import {
verifyRequest, keysFromEnv, turnstileConfigCheck, assertCanaryRecipient,
syntheticReport, sendBeacon, upstashReplayGuard,
} from "@domandigital/synthetic";
export async function POST(request: Request) {
const verified = await verifyRequest(request, {
keys: keysFromEnv(env), // empty => synthetic path off
host: "www.example.co.uk", // the public host
path: "/api/enquiry",
contentTypes: ["application/json"],
replayGuard: upstashReplayGuard({ url: env.UPSTASH_REDIS_REST_URL, token: env.UPSTASH_REDIS_REST_TOKEN }),
});
if (!verified.ok && verified.rejected) {
// Handle as an ordinary request (Turnstile still applies) and tell dd-checks.
ctx.waitUntil(sendBeacon(reporter, { form: "enquiry", code: "synthetic_rejected", reason: verified.reason }));
}
const synthetic = verified.ok ? verified.synthetic : null; // frozen; the only source of truth
// 1. same-origin 2. schema validation
// 3. synthetic ? turnstileConfigCheck({...}) : verify the Turnstile token
// 4. the real rate limiter, identifier `synthetic:<form>` when synthetic
// 5. full mode only: DB write (record_origin='synthetic', synthetic_run_id, purge_after),
// staff email redirected to canaryAddress("s", runId); submitter email to
// canaryAddress("u", runId) on the daily run only. assertCanaryRecipient() first.
// 6. skip SMS, CRM, marketing lists, trackLead; report them as "skipped".
// 7. respond with { ..., synthetic: syntheticReport(synthetic, stages) } only when synthetic
}
规则代码为您执行:
- 默认关闭. 无
DD_SYNTHETIC_SECRET(和DD_SYNTHETIC_KID每个请求都是普通的. - 签名前的形状 . 方法,主机,路径和内容类型在机构被散列前进行检查.
- 被拒绝的请求是普通请求从来没有失败.
verify从来没有扔过. - 需要回放守卫 并在签名后运行,所以一个被伪造的请求不能燃烧一个真实运行的id.
- 文体和查询从不读取 页:1
synthetic编辑SyntheticContext仅由已核实信头构建并被冻结.
重放守护选项 : upstashReplayGuard (单位:千美元)SET synth:<runId> NX EX 300),或
您自己的功能相对于一个独特的制约 synthetic_run_id数字 :
replayGuard: async (runId) => {
try { await db.insert(syntheticRuns).values({ syntheticRunId: runId }); return true; }
catch (e) { if (isUniqueViolation(e)) return false; throw e; }
}
旋转
一个网站最多持有两把钥匙: DD_SYNTHETIC_SECRET + 键 DD_SYNTHETIC_KID,以及
DD_SYNTHETIC_SECRET_NEXT + 键 DD_SYNTHETIC_KID_NEXT设置新密钥为
_NEXT 和一个新孩子, 切换平台,推广它, 然后删除
旧的 一个身份不明的孩子被拒绝了 秘诀为32个随机字节,基数64url:
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"
签名(dd-checks)
const { headers, runId } = await sign({
key: { kid: "example-2026-10", secret },
url: "https://www.example.co.uk/api/enquiry",
contentType: "application/json",
body: rawBodyString,
client: "example", form: "enquiry", mode: "probe",
});
runId 16 个字符 [a-z2-7]生成新鲜的,每一次来自
CSPRNG(英语:CSPRNG). 重放非曲直 然后进入金丝雀地址 所以每跑一次
是独一无二的 客户端的无一能键不能去除一个.
线条格式
标题 : X-DD-Synth: v1, (中文(简体) ), X-DD-Synth-Kid, (中文(简体) ), -Ts (unix 秒),(unix seconds),(英语:, -Run, (中文(简体) ),
-Mode (单位:千美元)probe 或者说 full, (中文(简体) ), -Client, (中文(简体) ), -Form, (中文(简体) ), -Sig: v1=<hex>。 。 。 .
签名是HMAC-SHA256 上面有32字节的秘密:
DD-SYNTH-V1\n<kid>\n<ts>\n<runId>\n<mode>\n<client>\n<form>\nPOST\n<host>\n<path>\n<content-type>\n<hex sha256(raw body)>
host 小写,并仅在 URL 有端口时包含端口. path 已经
查询字符串已删除. content-type 信头与发送的完全相同(a)
多段边界是它的一部分). 窗口:±120 s. 比较:恒定时间.
测试向量 发表于 test-vectors.json
(单位:千美元)@domandigital/synthetic/test-vectors.json- 一个测试秘密,每个案例
输入、词条、身体散列和预期签名。 他们
以 node:crypto 以 scripts/gen-vectors.mjs,独立于
这个包裹的代码。 任何其他执行( Python 检查器、 Go 服务)
必须复制它们.
收件人锁定
assertCanaryRecipient(submitterEmail); // throws SyntheticRecipientError
仅 @canary.domandigital.co.uk 通过,作为一个简单的地址。 显示名称,
列表、子域和外观类似域都失败。 在一切发生之前就叫它
发送.
旋转配置自查
turnstileConfigCheck({ siteKey, secretKey, expectedHostname, publicHost })
在经核实的合成请求中替换符号。 它回来了
{ ok, outcome },与 outcome 其中一个:
| 结果 | 含义 |
|---|---|
ok | 存在密钥, 主机名右侧, 被网站验证接受的秘密 |
missing_keys | 键或 TURNSTILE_EXPECTED_HOSTNAME 未设置 |
hostname_mismatch | TURNSTILE_EXPECTED_HOSTNAME已正常化的不是公共主机(在客户端网站被忽略了几个星期的失败) |
test_key_on_production | a 生产主机上的 Cloudflare 测试键 |
invalid-input-secret | 校对:soup |
siteverify_unexpected | 网站验证回答其他问题(例如 success 用于生产时的假币) |
siteverify_unreachable | 网络出错、 超时或非 JSON ; 不描述配置 |
Hostnames 比较小写与方案,端口,路径和后继点已删除;
www. 因为它是一个不同的宿主。 显示主机接受测试
键,因为运行浏览器的行程使用 Cloudflare 总是通过键.
灯塔和清理收据
const reporter = { key, client: "example", waitUntil: ctx.waitUntil.bind(ctx) };
sendBeacon(reporter, { form: "enquiry", code: "accepted" });
sendPurgeReceipt(reporter, { form: "enquiry", deleted: 24, remaining: 0 });
两者均用网站的钥匙签名(-Mode: beacon 编号 : receipt),员额改为
https://checks.domandigital.co.uk/beacon 财务报告和审定财务报表 /purge-receipt,为
火和遗忘,和 永不抛出或拒绝:一个糟糕的争论被放弃了. A 类
信标只持有代码(accepted, (中文(简体) ), hostname_mismatch, (中文(简体) ),
invalid-input-secret, (中文(简体) ), misconfigured, (中文(简体) ), service_unavailable, (中文(简体) ),
delivery_failed, (中文(简体) ), captcha_failed, (中文(简体) ), synthetic_rejected 加一个 reason, (中文(简体) ),
也从来不是个人资料.
让信头远离哨兵和日志
Sentry.init({ beforeSend: scrubSentryEvent, beforeSendTransaction: scrubSentryEvent });
logger.info(scrubText(headerDump));
const safe = scrubHeaders(request.headers);
发展
pnpm --filter @domandigital/synthetic test # Node, then workerd