> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibestrap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub 邀请分发

> 买家如何拿到源码 —— 付费门槛 + 只读 collaborator 邀请，自动加进你的私有 Org repo。

当客户购买 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 镜像：

```bash theme={null}
# 可选：dev / 买家面 repo 隔离
git push --mirror git@github.com:YourOrg/vibestrap.git
```

更新 Vibestrap 配置：

```ts theme={null}
// 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 permission** 设 `None`
* 可选：禁止 fork 私有 repo（防买家 fork 出去泄露）

### 3. 生成 fine-grained PAT

去 [https://github.com/settings/personal-access-tokens（用](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：

```bash theme={null}
GITHUB_INVITE_TOKEN=github_pat_11ABCDEF...
```

`src/github/invite.ts` 在请求时读它，rotate 不需要重启。

### 4. 本地验证

```bash theme={null}
# 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 写一行 `payment`：`status='paid'`，`scene=siteConfig.product.id`
3. 用户进 `/dashboard` —— 页面检测到付款记录，渲染买家视图，里面嵌着邀请表单
4. 用户输 GitHub 用户名，点 **Send invite**
5. server action 查 payment、调 GitHub、返回 reason，前端 toast 显示
6. 用户在 GitHub 邮箱接受邀请，然后：
   ```bash theme={null}
   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](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` 重新申请

## 源码链接

* API client：[`src/github/invite.ts`](https://github.com/xiaohu0x/vibestrap/blob/main/src/github/invite.ts)
* Server action：[`src/actions/send-github-invite.ts`](https://github.com/xiaohu0x/vibestrap/blob/main/src/actions/send-github-invite.ts)
* UI：[`src/app/[locale]/(app)/settings/purchases/`](https://github.com/xiaohu0x/vibestrap/tree/main/src/app/%5Blocale%5D/\(app\)/settings/purchases)
* 测试：[`tests/unit/github-invite.test.ts`](https://github.com/xiaohu0x/vibestrap/blob/main/tests/unit/github-invite.test.ts)
