DBX 官网 Cloudflare Worker 部署指南:贡献者证书 GitHub 认证与匿名 Issue 提交
【免费下载链接】dbx15MB,轻量级跨平台数据库客户端、数据库管理工具。支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server 等。15MB, lightweight, cross-platform database client. Supports MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server and more.项目地址: https://gitcode.com/t8y2/dbx
本文基于 DBX 官网(docs/目录下的 Next.js 静态站 + Cloudflare Worker)的部署文档 CONTRIBUTORS.md 与仓库内真实实现代码展开,系统讲解两类线上功能的完整配置流程:一是贡献者页面基于 GitHub OAuth 的身份验证与贡献证书资格判定,二是无需登录的匿名 Issue 提交(含滚动限流、AI 草稿生成、GitHub App 自动开 Issue 与 R2 截图存储)。读完本文,你可以独立为一个t8y2/dbx同类站点完成 Worker 的密钥配置、GitHub OAuth App / GitHub App 创建,并理解限流与草稿状态机的底层实现。
站点 Worker 总体结构:静态资源、API 路由与 Durable Object
在配置任何密钥之前,需要先理解 Worker 的职责边界。docs/wrangler.json 声明了整个部署单元的拓扑:
{ "name": "dbx", "compatibility_date": "2026-05-07", "main": "./worker.ts", "assets": { "directory": "./out", "binding": "ASSETS", "not_found_handling": "404-page", "run_worker_first": [ "/api/*", "/tr", "/tr/*", "/issue", "/issue/", "/issues", "/issues/", "/cn/issues", "/cn/issues/", "/en/issues", "/en/issues/" ] }, "r2_buckets": [ { "binding": "ISSUE_IMAGES", "bucket_name": "dbx" } ], "durable_objects": { "bindings": [ { "name": "ISSUE_LIMITER", "class_name": "IssueSubmissionLimiter" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["IssueSubmissionLimiter"] } ] }从这份配置可以读出三层结构:
- 静态资源层:
./out(Next.js 静态导出产物)通过ASSETS绑定提供,未命中路由返回站点自身的 404 页面; - Worker 拦截层:
run_worker_first列出的路径会先经过 worker.ts 处理——所有/api/*端点、已下线土耳其语文档路由/tr/*(308 重定向到英文),以及/issue、/issues等匿名反馈别名路由; - 有状态层:R2 bucket(Issue 截图存储)与一个名为
IssueSubmissionLimiter的 Durable Object(限流计数与草稿状态机),并使用 SQLite 存储迁移(v1)初始化。
worker.ts 的默认导出fetch入口即按此顺序分发:先处理 Issue 路由重定向与/api端点,未匹配的/api路径直接返回 JSON 404(注释说明这是为了防止 API 请求落入被边缘缓存的 HTML 404 页),其余请求回落ASSETS,并对/_next/static/与图片资源附加长期缓存头(worker.ts#L105-L109)。
贡献者页面:资格规则与 GitHub OAuth 证书认证
谁有资格领取贡献证书
贡献者页面(路由/[lang]/contributors,由 contributors/page.tsx 渲染)的数据来自构建期生成的静态快照 data/contributors.json。其资格规则在 CONTRIBUTORS.md 中有明确定义:只有向t8y2/dbx合并过至少一个 Pull Request 的账号才被判定为合格贡献者;提交(commit)数量仅作为附加展示指标,本身不授予资格。
这一规则由数据同步脚本 update-contributors.mjs 在构建期执行:脚本并行拉取仓库信息、/contributors?anon=0与所有已关闭 PR,逐条统计merged_at非空的 PR 数量,过滤掉机器人账号后,仅保留mergedPullRequests > 0的用户并排序输出。源码中的注释直接印证了文档描述:
// Commit counts enrich merged-PR contributors but never grant eligibility alone. const contributor = contributors.get(entry.login.toLowerCase()); if (contributor) contributor.commits = entry.contributions || 0;(update-contributors.mjs#L93-L98)
展示层的排序与去重逻辑同样在仓库中可查:按 commits、merged PR 数、login 字母序三级排序见 contributorActivity.ts 的sortContributorActivity;按小写 login 去重见 contributors.ts 的dedupeContributors。
创建 GitHub OAuth App
按 CONTRIBUTORS.md 的要求,创建 OAuth App 时填写:
- Homepage URL:
https://dbxio.com - Authorization callback URL:
https://dbxio.com/api/auth/github/callback
关键点是该 OAuth 流程不请求任何仓库 scope:Worker 换取 access token 后仅调用GET /user读取公开身份,随后立即丢弃 token(worker.ts#L258-L259 注释写明 “The access token is intentionally discarded after reading the public identity”)。这意味着站点只保存login、头像与个人主页链接,不会长期持有用户凭据。
配置 OAuth 相关 Worker 密钥
在docs/目录下为已部署的 Worker 配置:
pnpm dlx wrangler secret put GITHUB_CLIENT_ID pnpm dlx wrangler secret put GITHUB_CLIENT_SECRET pnpm dlx wrangler secret put SESSION_SECRETSESSION_SECRET应为随机生成的至少 32 字节值。它用于 HMAC-SHA256 签名两类无状态 Cookie:
| Cookie | 作用 | 有效期 | 签名载荷 |
|---|---|---|---|
dbx_oauth_state | 存放 OAuthstate+ PKCEverifier+ 回跳路径 | 10 分钟 | OAuthState |
dbx_contributor_session | 登录会话 | 7 天 | SessionUser(login、头像、主页 URL、过期时间) |
两个 Cookie 均为HttpOnly; Secure; SameSite=Lax(worker.ts#L181-L183)。
非生产回跳域名:GITHUB_OAUTH_CALLBACK_URL
生产环境的回跳地址默认拼接为${请求 origin}/api/auth/github/callback。如果测试环境的回跳域名与 OAuth App 注册的不一致,需要额外配置:
pnpm dlx wrangler secret put GITHUB_OAUTH_CALLBACK_URL其取值必须与 OAuth App 中注册的 callback 完全一致。源码中该密钥的读取逻辑见 worker.ts#L214:const callbackUrl = env.GITHUB_OAUTH_CALLBACK_URL || \${url.origin}/api/auth/github/callback`,且 authorize 与 token 交换两处请求都使用同一个callbackUrl,保证redirect_uri` 参数一致。
OAuth 完整流程(含 PKCE)
GET /api/auth/github/start的实现(worker.ts#L206-L231)比文档描述多出几层安全细节:
- 缺失
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET/SESSION_SECRET任一项时返回503(requiredConfig检查); - 生成随机
state与 48 字节verifier,按S256计算code_challenge一并发给 GitHub 授权端点——即启用了 PKCE; returnTo参数经sanitizeReturnTo校验:只接受以/开头且不是//的站内路径,否则回落到/en/contributors,杜绝开放重定向;- 回调端
finishOAuth(worker.ts#L233-L273)校验签名 Cookie 中state与 URL 参数一致且未过期,携带code_verifier换取 token,成功后清除 state Cookie、写入 7 天会话 Cookie 并 302 回跳。
上述行为的回归测试见 worker.test.ts:签名载荷的 round-trip 与篡改拒绝(test("signed OAuth payloads round-trip and reject tampering", ...))、//evil.example与绝对 URL 被重置为/en/contributors的断言(worker.test.ts#L11-L15)。
匿名 Issue 提交:限流、AI 草稿、GitHub App 与截图存储
路由与限流模型
/issue与/issues(含/cn/issues、/en/issues)路由对 GET 请求返回 308 重定向到按Accept-Language本地化的/cn/issue或/en/issue(issueRedirectPath,worker.ts#L530-L534,测试见 worker.test.ts#L17-L22)。文档明确说明:创建草稿时同时消耗 per-IP 与临时会话两条各 8 次/小时的滚动限额;最终提交不再消耗额度;且刻意不引入 Cloudflare Turnstile,防滥用完全依靠滚动窗口 + 会话签名 + Origin 校验(verifyIssueOrigin要求Origin头与请求 URL 同源)。
限额常量化定义在 issueSubmission.ts#L1-L12:
export const ISSUE_RATE_LIMIT = 8; export const ISSUE_RATE_WINDOW_MS = 60 * 60 * 1000; // 1 小时 export const ISSUE_DRAFT_TTL_MS = 30 * 60 * 1000; // 草稿 30 分钟 export const ISSUE_CLAIM_TTL_MS = 2 * 60 * 1000; // 提交占用 2 分钟滚动窗口的核心算法consumeRollingLimit(issueSubmission.ts#L193-L205)在 1 小时窗口内保留全部时间戳,超过 8 条即拒绝并返回最早的resetAt;Durable Object 侧还会为窗口末尾时间戳设置 alarm,窗口结束后自动deleteAll()清理(worker.ts#L628-L630),因此限额状态不会无限累积。测试用例直接验证了“8 次放行、第 9 次拒绝、1 小时后释放”这一行为(issueSubmission.test.ts#L13-L28)。
IP 与会话身份均不以明文存储:两者分别经HMAC-SHA256(secret, "ip:" + IP)与HMAC-SHA256(secret, "session:" + sessionId)哈希后,映射到独立的 Durable Object 实例(ip:<hash>/session:<hash>),Worker 再向各自实例的/limit/consume端点请求消耗额度(worker.ts#L314-L322、L413-L424)。
配置 GitHub App(Issues 写权限)
匿名反馈最终通过 GitHub App 以仓库身份发布 Issue。按 CONTRIBUTORS.md,GitHub App 应仅安装到t8y2/dbx,仓库权限只授予Issues: Read and write,然后配置:
pnpm dlx wrangler secret put GITHUB_APP_ID pnpm dlx wrangler secret put GITHUB_APP_PRIVATE_KEY_B64 pnpm dlx wrangler secret put ISSUE_RATE_LIMIT_SECRET参数要点:
GITHUB_APP_PRIVATE_KEY_B64:接受 GitHub App 下载 PEM 私钥后的 base64 单行值,生成命令base64 < private-key.pem | tr -d '\n';ISSUE_RATE_LIMIT_SECRET:独立的、至少 32 字节的随机值,用于会话 Cookie 与身份哈希签名;SESSION_SECRET只作为回退使用,目的是不破坏已部署的 Worker(回退逻辑见 worker.ts#L308-L312 的issueRuntimeSecret);- 源码对私钥做了兼容处理:支持
RSA PRIVATE KEY(PKCS#1)自动包裹成 PKCS#8 后再用 WebCrypto 加载(issueSubmission.ts#L130-L145)。
配置 OpenAI 兼容多模态 AI 提供方
pnpm dlx wrangler secret put ISSUE_AI_API_BASE pnpm dlx wrangler secret put ISSUE_AI_API_KEY pnpm dlx wrangler secret put ISSUE_AI_MODELISSUE_AI_API_BASE的写法有三种均可识别(issueAiEndpoint归一化逻辑,issueSubmission.ts#L239-L244):以主机名结尾、以/v1结尾、或直接以/chat/completions结尾。文档同时要求模型必须接受image_urldata URL——因为截图会以data:<mime>;base64,...形式内联进 user message(issueSubmission.ts#L326-L331)。
AI 请求的实际参数固定为temperature: 0.2、max_tokens: 6000,超时按是否带截图区分:纯文本 45 秒、含图 90 秒(issueAiRequestTimeoutMs,测试见 issueSubmission.test.ts#L59-L63)。系统提示词严格要求模型只保留描述与截图中可确认的事实、缺失信息标注“未提供”、对截图中的密码/Token/连接串脱敏(issueSubmission.ts#L250-L271),模型输出被解析为{type,title,summary,body}的 JSON 草稿(parseIssueAiResponse)。
R2 截图存储与公共地址
wrangler.json 将ISSUE_IMAGES绑定到既有的dbxR2 bucket。生成图片的公共 URL 默认发布于https://dl.dbxio.com;若 bucket 的公共访问域名变更,需设置密钥ISSUE_IMAGE_PUBLIC_BASE_URL覆盖默认值(worker.ts#L382)。对象键按issue-feedback/年/月/draftId/序号-UUID.扩展名组织(issueImageObjectKey,issueSubmission.ts#L465-L469),上传时写入public, max-age=31536000, immutable缓存头;上传中途失败会回滚已上传对象。截图本身有严格限制:最多 3 张、单张 ≤5MB、合计 ≤12MB,且只信任文件魔数(PNG/JPEG/WebP 头字节),不信任浏览器声明的 MIME(readIssueImages,issueSubmission.test.ts#L51-L57 有对应测试)。
ISSUE_GITHUB_REPOSITORY仅在针对其他仓库联调时才需要设置,生产默认t8y2/dbx(该默认值同样出现在 issueSubmission.ts#L403)。
提交链路:草稿状态机与幂等
一次完整的匿名提交包含两个端点,对应 Durable Object 中IssueSubmissionLimiter类的四个内部端点(worker.ts#L536-L631):
POST /api/issues/draft(worker.ts#L406-L464):创建/复用 24 小时临时会话 Cookie(SameSite=Strict)→ 校验 Origin → 消耗 IP 与会话两条滚动限额(任一超限返回 429 +retryAfter秒数)→ 校验描述(4~6000 字符)→ 调 AI 生成可编辑草稿 → 在会话的 Durable Object 中draft/create一条 30 分钟 TTL 的记录,返回draftId、preview与剩余限额;POST /api/issues/submit(worker.ts#L466-L524):不重复消耗限额 →draft/claim在 Durable Object 事务内把草稿从ready置为submitting(2 分钟占用锁),重复提交同一draftId会得到completed结果直接返回既有 Issue 链接,从而实现提交幂等;随后上传截图、生成带标题前缀([Bug]/[Feature]/[Question]/[Compatibility])与仓库 Issue 标签的正文,经 GitHub App JWT(RS256,iat=now-60、exp=now+540)换取仅issues:write权限的 installation token 创建 Issue;成功后draft/complete记录submitted状态与 Issue 编号;任何环节抛错都会清理已传截图并draft/release释放占用。
GitHub App 的 token 最小化值得注意:Worker 每次只申请“当前仓库 + issues write”的 installation token(issueSubmission.ts#L402-L426),且整个流程不使用用户身份,Issue 以仓库身份发布。
部署密钥速查与验证
把 CONTRIBUTORS.md 的全部配置项汇总如下(均在docs/目录执行wrangler secret put):
| 密钥 | 用途 | 取值要求 |
|---|---|---|
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET | 贡献者页面 OAuth 登录 | GitHub OAuth App 凭据;缺一即返回 503 |
SESSION_SECRET | 签名 state/会话 Cookie | 随机值 ≥32 字节 |
GITHUB_OAUTH_CALLBACK_URL | 非生产回跳域名覆盖 | 与 OAuth App 注册的 callback 一致 |
GITHUB_APP_ID | 匿名 Issue 发布 | 仅安装于t8y2/dbx的 App |
GITHUB_APP_PRIVATE_KEY_B64 | App 私钥 | base64 < private-key.pem \| tr -d '\n' |
ISSUE_RATE_LIMIT_SECRET | 限流会话签名/身份哈希 | 独立随机值 ≥32 字节;SESSION_SECRET仅作回退 |
ISSUE_AI_API_BASE | OpenAI 兼容端点 | 可止于 host、/v1或/chat/completions |
ISSUE_AI_API_KEY/ISSUE_AI_MODEL | 凭证与模型 | 模型需支持image_urldata URL |
ISSUE_IMAGE_PUBLIC_BASE_URL | 截图公共域名 | 默认https://dl.dbxio.com,仅在公共域名变更时设置 |
ISSUE_GITHUB_REPOSITORY | 联调目标仓库 | 仅联调时设置,生产默认t8y2/dbx |
改动后可借助仓库自带的测试验证关键行为:限流窗口(issueSubmission.test.ts)、AI 响应解析与标题前缀归一化、魔数检测,以及 Worker 层的签名载荷、开放重定向防护、Issue 路由 308 与静态缓存头(worker.test.ts)。
综上,DBX 官网把“贡献者证书”与“匿名反馈”两个看似独立的功能,统一收敛在同一个 Cloudflare Worker 里:OAuth 侧坚持“最小 scope + 即用即弃 token + 有状态签名 Cookie”,反馈侧则以 Durable Object 事务实现跨实例一致的滚动限流与幂等提交——两者的密钥、路由与存储绑定全部由 wrangler.json 与 CONTRIBUTORS.md 中的配置项显式声明,可按上表逐项完成部署。
【免费下载链接】dbx15MB,轻量级跨平台数据库客户端、数据库管理工具。支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server 等。15MB, lightweight, cross-platform database client. Supports MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server and more.项目地址: https://gitcode.com/t8y2/dbx
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考