feat(captcha): switchable multi-provider set + GeeTest v4 + Cap - #30
Conversation
Replace the single site-wide captcha provider with an ordered, switchable set: - captcha_providers[] — element 0 is the default, the rest are alternates a visitor can switch to (manually, on failure, or after a configurable timeout). The chosen provider is remembered in localStorage. - Each provider carries its own credentials; submissions name their provider and the server verifies against it, rejecting providers outside the enabled set. - Read-time migration seeds captcha_providers + per-provider keys from the legacy captcha_provider / shared key pair, so existing deployments keep working. New providers: - GeeTest v4 (SenseBot) — lib/geetest.ts, HMAC-signed server validation with a configurable fail-open/closed posture on GeeTest outage. - Cap (trycap.dev) — lib/cap.ts, embedded via capjs-core (KV-backed replay guard + redeem tokens, secret derived from the JWT secret) or external Cap Standalone. Aliases esbuild/javascript-obfuscator to a stub since capjs-core lazily imports them for instrumentation levels the embedded path never uses. Also: AdminSettings provider-set editor (default + alternates with reordering + per-provider panels), new config keys wired through allowlist/redaction/secret encryption/log redaction, EN+zh docs, CONTEXT.md glossary and ADR 0001.
审查者指南此 PR 将验证码从单一的旧版提供商迁移为一组按顺序排列、可手动切换的提供商,并支持按提供商进行验证以及向后兼容的读取时迁移;同时在所有受保护流程中加入 GeeTest v4 和嵌入式/外部 Cap 集成,并提供相应的管理员配置、安全处理、UI、类型、测试/构建接线、本地化和文档;此外,还引入了独立的按账户配置的 GPG 登录 TOTP 退出选项。 可切换验证码验证的时序图sequenceDiagram
actor Visitor
participant Captcha as Captcha UI
participant GatedPage as Gated flow
participant AuthRoute as Auth route
participant Middleware as verifyCaptchaToken
participant Provider as Selected provider verifier
GatedPage->>Captcha: Render captcha descriptor
Captcha->>Captcha: Select stored provider or default
Captcha-->>Visitor: Show selected widget
alt Verification succeeds
Visitor->>Captcha: Solve widget
Captcha->>GatedPage: onVerified(provider, proof)
GatedPage->>AuthRoute: Submit provider-specific proof
AuthRoute->>Middleware: verifyCaptchaToken(submission, ip, env)
Middleware->>Middleware: Check provider is enabled
Middleware->>Provider: Dispatch provider verification
Provider-->>Middleware: Verification result
Middleware-->>AuthRoute: CaptchaResult
AuthRoute-->>Visitor: Continue gated action
else Verification fails or timeout expires
Captcha-->>Visitor: Reveal switch control
Visitor->>Captcha: Switch provider
Captcha->>Captcha: storeProvider(provider)
Captcha-->>Visitor: Render alternate widget
end
嵌入式 Cap 挑战兑换的时序图sequenceDiagram
actor Visitor
participant CapWidget as Cap widget
participant Worker as Prism Worker
participant CapCore as capjs-core
participant KV as KV_CACHE
participant AuthRoute as Gated auth route
participant Captcha as verifyCaptchaToken
CapWidget->>Worker: POST /api/auth/cap/challenge
Worker->>CapCore: issueCapChallenge()
CapCore-->>Worker: Signed challenge JWT
Worker-->>CapWidget: Challenge
Visitor->>CapWidget: Solve proof-of-work
CapWidget->>Worker: POST /api/auth/cap/redeem
Worker->>CapCore: redeemCapChallenge()
CapCore->>KV: consumeNonce
KV-->>CapCore: Nonce available
CapCore->>KV: Store redeem token
CapCore-->>Worker: Opaque redeem token
Worker-->>CapWidget: Redeem token
Visitor->>AuthRoute: Submit cap_token and provider
AuthRoute->>Captcha: verifyCaptchaToken()
Captcha->>KV: Read and delete token
KV-->>Captcha: Token validity
Captcha-->>AuthRoute: Verification result
旧版验证码配置迁移流程图flowchart TD
Load[Load SiteConfig] --> HasSet{captcha_providers written?}
HasSet -->|Yes| Active[Use ordered provider set]
HasSet -->|No| Legacy{Legacy provider enabled?}
Legacy -->|No| Off[Captcha remains disabled]
Legacy -->|Yes| Seed[Seed captcha_providers with legacy provider]
Seed --> Credentials[Copy legacy shared credentials to provider fields]
Credentials --> Active
Active --> Save[Admin saves settings]
Save --> NewShape[Persist new configuration shape]
文件级变更
提示和命令与 Sourcery 交互
自定义使用体验访问你的控制面板:
获取帮助Original review guide in EnglishReviewer's GuideThis PR migrates captcha from one legacy provider to an ordered, manually switchable provider set with per-provider verification and backward-compatible read-time migration, adds GeeTest v4 and embedded/external Cap integrations across all gated flows, and supplies corresponding admin configuration, security handling, UI, types, tests/build wiring, localization, and documentation; it also introduces a separate per-account GPG-login TOTP opt-out. Sequence diagram for switchable captcha verificationsequenceDiagram
actor Visitor
participant Captcha as Captcha UI
participant GatedPage as Gated flow
participant AuthRoute as Auth route
participant Middleware as verifyCaptchaToken
participant Provider as Selected provider verifier
GatedPage->>Captcha: Render captcha descriptor
Captcha->>Captcha: Select stored provider or default
Captcha-->>Visitor: Show selected widget
alt Verification succeeds
Visitor->>Captcha: Solve widget
Captcha->>GatedPage: onVerified(provider, proof)
GatedPage->>AuthRoute: Submit provider-specific proof
AuthRoute->>Middleware: verifyCaptchaToken(submission, ip, env)
Middleware->>Middleware: Check provider is enabled
Middleware->>Provider: Dispatch provider verification
Provider-->>Middleware: Verification result
Middleware-->>AuthRoute: CaptchaResult
AuthRoute-->>Visitor: Continue gated action
else Verification fails or timeout expires
Captcha-->>Visitor: Reveal switch control
Visitor->>Captcha: Switch provider
Captcha->>Captcha: storeProvider(provider)
Captcha-->>Visitor: Render alternate widget
end
Sequence diagram for embedded Cap challenge redemptionsequenceDiagram
actor Visitor
participant CapWidget as Cap widget
participant Worker as Prism Worker
participant CapCore as capjs-core
participant KV as KV_CACHE
participant AuthRoute as Gated auth route
participant Captcha as verifyCaptchaToken
CapWidget->>Worker: POST /api/auth/cap/challenge
Worker->>CapCore: issueCapChallenge()
CapCore-->>Worker: Signed challenge JWT
Worker-->>CapWidget: Challenge
Visitor->>CapWidget: Solve proof-of-work
CapWidget->>Worker: POST /api/auth/cap/redeem
Worker->>CapCore: redeemCapChallenge()
CapCore->>KV: consumeNonce
KV-->>CapCore: Nonce available
CapCore->>KV: Store redeem token
CapCore-->>Worker: Opaque redeem token
Worker-->>CapWidget: Redeem token
Visitor->>AuthRoute: Submit cap_token and provider
AuthRoute->>Captcha: verifyCaptchaToken()
Captcha->>KV: Read and delete token
KV-->>Captcha: Token validity
Captcha-->>AuthRoute: Verification result
Flow diagram for legacy captcha configuration migrationflowchart TD
Load[Load SiteConfig] --> HasSet{captcha_providers written?}
HasSet -->|Yes| Active[Use ordered provider set]
HasSet -->|No| Legacy{Legacy provider enabled?}
Legacy -->|No| Off[Captcha remains disabled]
Legacy -->|Yes| Seed[Seed captcha_providers with legacy provider]
Seed --> Credentials[Copy legacy shared credentials to provider fields]
Credentials --> Active
Active --> Save[Admin saves settings]
Save --> NewShape[Persist new configuration shape]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Deploying prism-docs with
|
| Latest commit: |
c52cd8f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2ae6d650.siiway-prism.pages.dev |
| Branch Preview URL: | https://feat-captcha-enhance-and-2fa.siiway-prism.pages.dev |
There was a problem hiding this comment.
嘿——我发现了 3 个问题
面向 AI Agent 的提示
请处理本次代码审查中的评论:
## 单独评论
### 评论 1
<location path="src/components/Captcha.tsx" line_range="297-301" />
<code_context>
+ onError?.(t("captcha.geetestFailed"));
+ return;
+ }
+ initGeetest4(
+ { captchaId: captcha.geetest_captcha_id, product: "bind" },
+ (captchaObj) => {
+ geetestObjRef.current = captchaObj;
+ captchaObj.onReady(() => setGeetestReady(true));
+ captchaObj.onSuccess(() => {
+ const result = captchaObj.getValidate();
</code_context>
<issue_to_address>
**问题 (bug_risk):** GeeTest 对象从未传递给其声明的 `appendTo` 方法,因此 v4 小组件不会挂载到容器中,其 ready 回调也不会触发;唯一的验证按钮会一直处于禁用状态。
**触发条件:** GeeTest 是当前启用的提供商时。
**建议修复:** 初始化后调用 `captchaObj.appendTo(containerRef.current)` 或文档中指定的选择器,并使用该提供商支持的交互 API。
</issue_to_address>
### 评论 2
<location path="worker/lib/cap.ts" line_range="86-92" />
<code_context>
+ const result = await validateChallenge(secret, body, {
+ scope: CAP_SCOPE,
+ tokenTtlMs: TOKEN_TTL_MS,
+ consumeNonce: async (sigHex, ttlMs) => {
+ const key = `${NONCE_PREFIX}${sigHex}`;
+ if (await env.KV_CACHE.get(key)) return false;
+ await env.KV_CACHE.put(key, "1", {
+ expirationTtl: Math.ceil(ttlMs / 1000),
+ });
+ return true;
+ },
+ });
</code_context>
<issue_to_address>
**问题 (bug_risk):** 内置的 Cap 单次使用检查不是原子的:并发请求可能同时观察到 KV 中不存在 nonce 或令牌,随后同时写入/删除它,并成功兑换同一个 challenge 或令牌。
**触发条件:** 当针对同一个 Cap challenge 或兑换令牌的两个请求,在 KV 状态一致可见之前到达时。
**建议修复:** 使用原子比较并设置(compare-and-set)操作、由 Durable Object 支持的消费操作,或以其他方式串行化检查和消费操作,而不是分别执行 KV 读取和写入。
</issue_to_address>
### 评论 3
<location path="worker/lib/geetest.ts" line_range="69-72" />
<code_context>
+ failOpen: boolean,
+): Promise<boolean> {
+ const { lot_number, captcha_output, pass_token, gen_time } = output;
+ if (!lot_number || !captcha_output || !pass_token || !gen_time) {
+ // No usable output at all — nothing to validate. Governed by policy.
+ return failOpen;
+ }
+
</code_context>
<issue_to_address>
**🚨 问题 (security):** 启用 `geetest_fail_open` 后,包含缺失或格式错误的 GeeTest 字段的提交会在未联系 GeeTest 的情况下被接受,攻击者可以通过发送空的 `geetest` 对象绕过验证码。
**触发条件:** 管理员选择 GeeTest 故障放行模式,且请求未提供可用的 GeeTest 输出时。
**建议修复:** 始终拒绝结构无效或缺失的输出;仅在确认 GeeTest 传输层或服务发生故障时应用故障放行。
```suggestion
if (!lot_number || !captcha_output || !pass_token || !gen_time) {
// Structurally invalid or missing output is always rejected.
return false;
}
```
</issue_to_address>Original comment in English
Hey - I've found 3 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/components/Captcha.tsx" line_range="297-301" />
<code_context>
+ onError?.(t("captcha.geetestFailed"));
+ return;
+ }
+ initGeetest4(
+ { captchaId: captcha.geetest_captcha_id, product: "bind" },
+ (captchaObj) => {
+ geetestObjRef.current = captchaObj;
+ captchaObj.onReady(() => setGeetestReady(true));
+ captchaObj.onSuccess(() => {
+ const result = captchaObj.getValidate();
</code_context>
<issue_to_address>
**issue (bug_risk):** The GeeTest object is never passed to its declared `appendTo` method, so the v4 widget is not mounted into the container and its ready callback does not fire; the only verification button remains disabled indefinitely.
**Triggers:** When GeeTest is the active provider.
**Suggested fix:** Call `captchaObj.appendTo(containerRef.current)` or the documented selector after initialization, and use the provider's supported interaction API.
</issue_to_address>
### Comment 2
<location path="worker/lib/cap.ts" line_range="86-92" />
<code_context>
+ const result = await validateChallenge(secret, body, {
+ scope: CAP_SCOPE,
+ tokenTtlMs: TOKEN_TTL_MS,
+ consumeNonce: async (sigHex, ttlMs) => {
+ const key = `${NONCE_PREFIX}${sigHex}`;
+ if (await env.KV_CACHE.get(key)) return false;
+ await env.KV_CACHE.put(key, "1", {
+ expirationTtl: Math.ceil(ttlMs / 1000),
+ });
+ return true;
+ },
+ });
</code_context>
<issue_to_address>
**issue (bug_risk):** The embedded Cap single-use checks are non-atomic: concurrent requests can both observe an absent KV nonce or token, then both write/delete it and successfully redeem the same challenge or token.
**Triggers:** When two requests for the same Cap challenge or redeem token arrive before KV state is consistently visible.
**Suggested fix:** Use an atomic compare-and-set/ Durable Object-backed consume operation, or otherwise serialize the check-and-consume operation instead of separate KV reads and writes.
</issue_to_address>
### Comment 3
<location path="worker/lib/geetest.ts" line_range="69-72" />
<code_context>
+ failOpen: boolean,
+): Promise<boolean> {
+ const { lot_number, captcha_output, pass_token, gen_time } = output;
+ if (!lot_number || !captcha_output || !pass_token || !gen_time) {
+ // No usable output at all — nothing to validate. Governed by policy.
+ return failOpen;
+ }
+
</code_context>
<issue_to_address>
**🚨 issue (security):** With `geetest_fail_open` enabled, a submission containing missing or malformed GeeTest fields is accepted without contacting GeeTest, allowing an attacker to bypass the captcha by sending an empty `geetest` object.
**Triggers:** When administrators choose GeeTest fail-open mode and a request supplies no usable GeeTest output.
**Suggested fix:** Reject structurally invalid or missing output unconditionally; apply fail-open only to a confirmed GeeTest transport/service outage.
```suggestion
if (!lot_number || !captcha_output || !pass_token || !gen_time) {
// Structurally invalid or missing output is always rejected.
return false;
}
```
</issue_to_address>|
Preview deployed: https://prism-preview.siiway.workers.dev (shared preview Worker + database, so the newest PR deploy is what is live there). |
- geetest: reject missing/malformed output unconditionally; only apply fail-open to a confirmed GeeTest transport/service outage, closing a bypass where an empty geetest object passed when fail-open was enabled (security). - cap: make the embedded single-use checks atomic via the D1 replay-claim table (claimReplayValue) instead of racy KV get-then-put — the challenge nonce and the redeem token are now consumed exactly once under concurrency. KV still proves token authenticity/expiry; the atomic claim is the single-use gate. - geetest widget: mount with product "popup" + appendTo(container) so the widget actually renders and onReady fires; removed the disconnected manual verify button.
|
Thanks @sourcery-ai — addressed all three in 093f4a3:
|
|
🤔 |
i dont want adr for now
Expand CONTEXT.md from the captcha-only glossary into the full project vocabulary — identity (user, team-as-user, restricted account), team structure (sub-team vs member group, effective role, site floor), apps & providers (OAuth app vs source, first-party/official/verified/trusted, public client), step-up/sudo, scopes & tokens, invitations, domains, captcha, and governance (audit log, notice, notification ruleset). Add an AGENTS.md 'Domain model' section directing agents to read and keep CONTEXT.md current.
Summary
Evolves the captcha system from a single site-wide provider into an ordered, switchable set of providers, and adds GeeTest v4 and Cap as new providers.
captcha_providers[]: element 0 is the default rendered first; the rest are alternates a visitor can switch to. The switch control is manual, revealed on a verification failure, and revealed aftercaptcha_switch_timeout_seconds(default 15s,0= failure-only). Switching never happens automatically; the chosen provider is remembered inlocalStorage.provider; the server verifies against that provider and rejects any provider not in the enabled set.captcha_providers+ per-provider credentials from the legacycaptcha_provider+ shared key pair, so existing deployments keep working until the settings are next saved.New providers
worker/lib/geetest.ts) — HMAC-signed server validation;geetest_fail_openchooses the posture on a GeeTest outage (default: fail closed).worker/lib/cap.ts) — embedded viacapjs-core(stateless signed-JWT challenges; single-use nonce + redeem token inKV_CACHE; HMAC secret derived from the JWT secret; routesPOST /api/auth/cap/{challenge,redeem}) or external Cap Standalone.esbuild/javascript-obfuscatorare aliased to a stub inwrangler.jsoncbecausecapjs-corelazily imports them for instrumentation obfuscation levels the embedded path never uses.The legacy Rust→WASM
powprovider is retained (deprecated in favour of Cap).Changes
verifyCaptchaTokenrewrite (provider discriminator + enabled-set check + dispatch),captchaPublic.tspublic descriptor, Cap challenge/redeem routes, config defaults + migration,SENSITIVE_CONFIG_KEYS/ admin allowlist / redaction / log redaction / secret-migration lists.Captcha.tsxmulti-provider + switch UI + localStorage; GeeTest v4 + Cap renderers; 5 gated pages updated (login, register, join, verify-choose, 2FA); AdminSettings provider-set editor (default + alternates with reorder + per-provider panels).configuration,architecture,admin+ zh mirrors),CONTEXT.mdglossary, ADR0001-switchable-captcha-set.capjs-core,@cap.js/widget.Testing
bunx tsc -b✅ ·bun run lint✅ ·bun test(84 pass) ✅bun run build✅ ·bun run docs:build✅wrangler deploy --dry-runbundles cleanly with the capjs-core alias ✅Notes for reviewers
@cap.js/serveris intentionally not used (it needs a filesystem and doesn't run on Workers);capjs-coreis the Worker-compatible library.Sourcery 摘要
将验证码保护演进为向后兼容、按顺序排列的多提供商系统,并支持访客切换以及 GeeTest v4 和 Cap。
新功能:
错误修复:
增强功能:
构建:
文档:
测试:
杂项:
Original summary in English
Sourcery 摘要
将验证码保护机制演进为向后兼容的有序提供商集合,支持访客切换、GeeTest v4 和 Cap,并提供安全的按提供商配置。
新功能:
错误修复:
增强功能:
构建:
文档:
测试:
杂项:
Original summary in English
Sourcery 摘要
将验证码保护机制演进为向后兼容的、有序提供商集合,支持访客切换、GeeTest v4 和 Cap,并提供安全的提供商级配置。
新功能:
错误修复:
增强功能:
构建:
文档:
测试:
杂项:
Original summary in English
Sourcery 摘要
将验证码保护演进为向后兼容、有序的提供商集合,支持访客切换、GeeTest v4 和 Cap,并提供安全的提供商专属管理功能。
新功能:
错误修复:
增强功能:
构建:
文档:
测试:
杂项:
Original summary in English
Summary by Sourcery
Evolve captcha protection into a backward-compatible, ordered provider set with visitor switching, GeeTest v4 and Cap support, and secure provider-specific administration.
New Features:
Bug Fixes:
Enhancements:
Build:
Documentation:
Tests:
Chores: