跳转到主要内容
当客户购买 Vibestrap 后,源码不是做成 zip 让他下载,而是他在 /settings/purchases 填自己的 GitHub 用户名 → 后端把他作为只读(pull)collaborator 邀请进你的私有 Org repo。 他接受邀请后 git clone,从此 git pull 就能拉你后续的所有版本。 这个设计不是 “license key + 签名下载链接”。zip 包发出去那一刻就过期了, 但 GitHub 邀请会跟着你一起成长。

门槛是怎么挡的

三层架构,故意解耦。整个流程只有一条业务规则:用户必须付过 Vibestrap 的钱。 其他都是水管。
[买家填 GitHub 用户名] → 点击按钮

sendGitHubInviteAction (src/actions/send-github-invite.ts)
  ├─ next-safe-action 校验输入
  ├─ Gate: SELECT FROM payment WHERE userId=? AND status='paid'
  │                              AND scene IN (product.id, 'vibestrap-lifetime')
  ├─ 没付钱 → 返回 { ok: false, reason: 'not_paid' }
  └─ 调 inviteCollaborator(username) ↓
                                       inviteCollaborator (src/github/invite.ts)
                                       ├─ PUT /repos/{owner}/{repo}/collaborators/{username}
                                       │  body: { "permission": "pull" }
                                       ├─ 翻译 GitHub 状态码:
                                       │    201 → invited
                                       │    204 / 422 → already_invited
                                       │    404 → invalid_username
                                       │    401 / 403 → forbidden
                                       │    429 → rate_limited
                                       └─ 返回 InviteResult 联合类型
买家看到的是 Sonner toast,根据 result reason 显示对应文案。

一次性配置

1. 用 Organization repo(不能是个人 repo)

GitHub 的 permission: 'pull' 字段在个人 repo 上会被悄悄忽略 —— 任何被加进个人 repo 的 collaborator 自动有 push 权限,与 API body 无关。必须用 Organization repo。 如果你的开发工作在个人 repo,把 Org repo 当作 release 镜像:
# 可选:dev / 买家面 repo 隔离
git push --mirror git@github.com:YourOrg/vibestrap.git
更新 Vibestrap 配置:
// src/config/site.ts
github: {
  inviteRepoOwner: 'YourOrg',     // ← Organization 名字
  inviteRepoName: 'vibestrap',    // ← 私有 repo
}

2. 锁住 Org

进 Org 设置 → Member privileges:
  • 取消勾选 “Allow members to delete or transfer repositories” —— 即使 token 有 Administration:write,也调不了 DELETE /repos
  • Default repository permissionNone
  • 可选:禁止 fork 私有 repo(防买家 fork 出去泄露)

3. 生成 fine-grained PAT

https://github.com/settings/personal-access-tokens(用 Org 成员账号登录, 建议是个 bot 账号,不是你个人账号):
  • Resource owner:你的 Org
  • Repository access:只选买家 repo
  • Permissions:
    • Metadata: Read-only(GitHub 强制必填)
    • Administration: Read and write(Add a collaborator 这个 endpoint 要求这个权限 —— 这是 GitHub 提供的最窄选项;防误用靠 Org 级”禁删”配置)
  • Expiration:1 年,日历提醒 60 天后 rotate
复制 token(只出现一次),粘进 prod env:
GITHUB_INVITE_TOKEN=github_pat_11ABCDEF...
src/github/invite.ts 在请求时读它,rotate 不需要重启。

4. 本地验证

# Dev shell
GITHUB_INVITE_TOKEN=ghp_xxx pnpm dev

# 另一个 shell
curl -X PUT \
  -H "Authorization: Bearer $GITHUB_INVITE_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/YourOrg/vibestrap/collaborators/your-test-account \
  -d '{"permission":"pull"}'
201 = 成功,204 = 已经是 collaborator,404 = 用户名错,403 = token 权限不对。

买家在线上的流程

  1. 用户注册、付款(Stripe)
  2. Webhook 写一行 paymentstatus='paid'scene=siteConfig.product.id
  3. 用户进 /dashboard —— 页面检测到付款记录,渲染买家视图,里面嵌着邀请表单
  4. 用户输 GitHub 用户名,点 Send invite
  5. server action 查 payment、调 GitHub、返回 reason,前端 toast 显示
  6. 用户在 GitHub 邮箱接受邀请,然后:
    git clone git@github.com:YourOrg/vibestrap.git
    
如果邮件丢了或要换用户名,再填一次再点 → 服务端是幂等的。

Rotation runbook

每 60 天一次(GitHub 90 天提醒邮件之前):
  1. 生成新 PAT,scope 一致
  2. 更 prod env(GITHUB_INVITE_TOKEN
  3. 重新部署
  4. 拿测试账号试一次邀请
  5. 验证完了去 https://github.com/settings/personal-access-tokens revoke 旧 token

排错

  • 买家说”not_paid”但其实付了 —— 检查 payment.scene 是否跟 siteConfig.product.id 完全一样 (或 'vibestrap-lifetime' 这个旧别名)。SQL 查一下
  • 买家说”invalid_username” —— 多半拼错了。或者带了 @ 前缀。本地验证一下
  • 所有 invite 都返 “forbidden” —— PAT 过期或 revoke 了,重新生成
  • 买家收到邀请但发现没 push 权限 —— 这就是设计目标:permission: 'pull' 在 Org repo 上 只给 clone,push 会被 remote rejected
  • 为啥不做 zip 下载? —— zip 包跟不上你后续 release。买家要的是 git pull,不是 下载完第二天就过时的 tarball

从老的 license-key 流程迁移

之前如果你跑过 license 表和 LICENSE_DOWNLOAD_URL
  • 两个都没了。Schema migration 删掉 license 表;env var 移除;/api/license/download 路由删了
  • sendGitHubInviteAction 里查 payment 表的 gate 替换了原本的查 license 行
  • 老买家(如果有)—— 手动 curl 邀请,或让他们去 /settings/purchases 重新申请

源码链接