我如何构建了一个系统,自动出售对私有 GitHub Repo 的访问权限
Source: Dev.to
TL;DR
我需要出售对私有 GitHub 仓库的访问权限。ZIP 下载无法获得更新,许可证密钥过于繁琐,而且多个支付提供商拒绝了我的业务。于是我构建了一个全自动系统来处理整个流程:
- Payment webhook fires → signature verified
- GitHub username captured via OAuth
- Collaborator invite sent in seconds
它运行得很好,但事实证明 Polar.sh 已经内置了此功能,而且做得更好。我最终没有使用自己的版本,但在构建过程中学到了很多。
Source: …
问题
我构建了一个想要出售的脚手架。产品已经完成——难点在于如何实际交付它。
你创建了一个入门套件、脚手架、模板,开发者愿意为此付费。接下来怎么办?付款后如何把它交付给他们?
我评估的选项
| 选项 | 优点 | 缺点 |
|---|---|---|
| Zip 文件下载 | 简单,开箱即用 | 没有更新,没有 git 历史,无法 pull/diff |
| License‑key 限制 | 可以锁定访问 | 需要自定义 CLI + 基础设施——过度复杂 |
| npm 私有包 | 开发者熟悉 | 需要注册表认证 → 摩擦导致转化率下降 |
| GitHub Sponsors(私有仓库) | 内置 GitHub 访问权限 | 只能订阅,不适合一次性购买 |
| 通过支付平台手动交付 | 少量销售时可行 | 规模化后几乎不可能(例如凌晨 2 点的第 3 笔订单) |
我真正想要的是极其简单:客户付款 → 客户立即自动获得私有仓库的访问权限。
支付提供商障碍
我的注册公司是一家设计与咨询公司。我尝试在 Lemon Squeezy 以及其他几个平台上注册以销售数字产品,但它们拒绝了我。
- 我说明我在销售一次性数字产品(模板代码、入门套件),而不是咨询服务。
- 拒绝仍然存在,而且一些平台甚至相当无礼。
那时我只知道我需要一个能够在付款时触发 webhook的提供商,这样我就可以自行自动化其余流程。
灵感
我发现了一个做类似事情的项目:
- 付款 webhook → GitHub 合作者邀请
- 私有仓库,只读访问(克隆 + 拉取更新,完整的 git 历史)
他们收取了 30 美元。我想自己动手做——一举两得。
为什么可行
- GitHub API 允许以编程方式添加协作者。
- 任何支付提供商 只要触发 webhook 就可以启动此流程。
- 权限可以设置为 pull(只读),这样客户可以克隆但不能推送。
Source: …
我的解决方案架构
一个 独立的 Next.js 应用,不依赖特定支付提供商。
User clicks "Buy"
|
GitHub OAuth (capture username)
|
Payment checkout
|
Webhook fires → verify signature → invite to repo → send email
|
Customer has access in seconds
主要组件
| 组件 | 描述 |
|---|---|
| GitHub OAuth | 在结账前获取买家的 GitHub 用户名 |
| Webhook handler | 接收付款确认,验证 HMAC‑SHA256 签名 |
| GitHub API client | 自动发送仓库邀请 |
| PostgreSQL | 记录每位客户(共 33 个字段) |
| Resend | 发送欢迎邮件 |
详细流程
1. 在 checkout 之前捕获 GitHub 用户名
支付提供商并不知道买家的 GitHub 用户名,所以我们必须先捕获它。
// OAuth 回调后,带着 GitHub 信息重定向到 checkout
const checkoutUrl = new URL(CHECKOUT_URL);
checkoutUrl.searchParams.set('gh_username', githubUser.login);
checkoutUrl.searchParams.set('gh_user_id', String(githubUser.id));
支付提供商会在 webhook 负载中包含这些字段,从而让我们在付款完成后准确知道要邀请谁。
2. 验证 webhook 签名
任何人都可以向你的 webhook 端点发送 POST 请求。如果不进行验证,攻击者就可以免费获取访问权限。
import { createHmac, timingSafeEqual } from 'crypto';
function verifySignature(payload: string, signature: string): boolean {
const expected = createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
注意事项
- 使用 原始请求体,而不是解析后的 JSON —— 重新序列化会改变空白字符并导致签名失效。
- 使用
timingSafeEqual而不是===—— 直接的字符串比较会泄露时间信息。
3. UPSERT 客户记录
客户可能会购买升级或第二件商品。如果没有 UPSERT,第二个 webhook 会因邮箱唯一约束冲突而失败。
INSERT INTO customers (email, name, amount_paid, ...)
VALUES (...)
ON CONFLICT (email) DO UPDATE SET
amount_paid = EXCLUDED.amount_paid,
order_id = EXCLUDED.order_id,
updated_at = NOW();
4. 处理待处理的邀请
如果客户没有接受邀请,邀请会保持 pending 状态。超过 50 个待处理邀请 后,GitHub 会完全阻止新的邀请。
- 我会跟踪邀请状态。
- 我构建了一个后台面板,用于监控待处理邀请并手动重新发送或撤销它们。
5. 对 GitHub API 失败进行重试
GitHub API 可能会宕机、触发速率限制或出现网络波动。为你的模板付费的客户不应该只看到错误并被要求联系支持。
指数退避策略
| 尝试次数 | 延迟 |
|---|---|
| 1 | 1 秒 |
| 2 | 2 秒 |
| 3 | 4 秒 |
| 4 | 8 秒 |
| … | … |
| 10 | 1 小时 |
错误分类
- 可重试 – 网络超时、速率限制响应、5xx 错误 → 进入退避循环。
- 不可重试 – 4xx 客户端错误(例如无效负载) → 记录并报警。
教训
- 支付提供商 对您出售的数字商品类型可能挑剔。
- 签名验证 必须使用原始负载并进行时间安全比较。
- UPSERT(或等价操作)可防止买家多次购买时出现键重复错误。
- 未处理的 GitHub 邀请 可能会悄悄阻塞您——请监控并清理它们。
- 健壮的重试逻辑 在依赖第三方 API 时至关重要。
结论
我构建了一个全自动、与供应商无关的系统,能够:
- 通过 OAuth 捕获买家的 GitHub 用户名。
- 监听支付 webhook,并安全地进行验证。
- 将买家以 只读协作者 的身份邀请到私有仓库。
- 发送包含仓库链接的欢迎邮件。
它能够正常工作,但 Polar.sh 已经开箱即用地提供了此功能,且实现得更好,因此我最终转而使用了他们的服务。尽管如此,这次经历让我深入了解了 OAuth、Webhook 安全、GitHub API 以及可靠的自动化模式。
带退避的队列
- 永久失败(例如 用户未找到、仓库未找到、
401/403)→ 发送到死信队列进行人工审查。 - 在 10 次失败尝试 后,项目会移至死信队列,并且我会收到通知。
GitHub 令牌交换端点
正确的端点是 https://github.com,而不是 https://api.github.com。
# WRONG – returns an HTML login page
POST https://api.github.com/login/oauth/access_token
# CORRECT – returns the token
POST https://github.com/login/oauth/access_token
我在使用 GitHub API 时形成的肌肉记忆让我总是指向 API 子域名,但 OAuth 流程实际上位于主站点。
技术栈概览
| 组件 | 技术 |
|---|---|
| 框架 | Next.js 16 (App Router) |
| 语言 | TypeScript(严格模式) |
| 数据库 | PostgreSQL on Neon |
| GitHub API | Octokit |
| 邮件 | Resend |
| 验证 | Zod |
| 测试 | Vitest(全部通过) |
构建后可在任何托管服务商上运行。目前在 Vercel 免费层上运行,适用于低流量。
为什么这种方法有真正的优势
- 即时更新 – 将修复推送到
main;每位客户只需git pull– 无需重新下载 zip 文件。 - 完整的 git 历史 – 客户可以查看为何以这种方式构建。
- 默认只读 –
pull权限让他们可以克隆但不能向仓库推送。 - 无需额外工具 – 只需要
git;不需要许可证密钥、自定义 CLI 或注册表认证。 - 可撤销访问 – 需要收回费用?只需一次 API 调用即可移除协作者。
缺点
- 50 个待处理邀请的上限 – GitHub 限制未完成邀请的数量。
- 以 GitHub 为中心 – 绑定在 GitHub 上;不是通用解决方案。
对于向开发者销售开发者工具而言,这些缺点通常是可以接受的。
我最终改用的方案
在构建工具的过程中,我改用了 Polar.sh 来处理支付,因为他们接受了我的业务(而 Lemon Squeezy 不接受)。他们的平台已经涵盖了我原本要重新实现的所有功能:
- 捕获 GitHub 用户名
- 仓库邀请
- 访问管理
简而言之,Polar 自动化了我想要构建的完整流程:
- 客户付款。
- Polar 捕获他们的 GitHub 用户名。
- Polar 发送仓库邀请。
- 客户获得访问权限。
我只需要获得批准,然后就可以打开此功能。
反思
我的工具可以正常工作:所有测试通过,流水线运行正常,但我从未需要部署它。不过,我学到了很多:
- Webhook 签名验证
- OAuth 流程
- 带指数退避的重试系统
- GitHub 合作者 API 的实际工作方式
这些细节是教程中学不到的。
开放式问题
我之所以构建它,是因为看到有人为类似的解决方案收取 $30,于是觉得自己也可以做一个。
- 真的会有人觉得这有用吗?
- 如果你在出售脚手架或入门套件,并且不想被锁定在 Polar 的内置解决方案中,这样的独立工具是否值得?
- 你希望它开源吗?
请在评论中告诉我。我正在决定是发布它(开源/“请给我买杯咖啡”)还是仅作为学习项目保留。出售它似乎有点没有意义,但构建过程很有趣。
TL;DR
使用 Next.js、TypeScript 构建,并花了大量时间阅读 webhook 文档。 🚀