docs: design autumn registration sync

This commit is contained in:
2026-07-02 04:00:45 -07:00
parent 8b32529749
commit e59805490b

View File

@@ -0,0 +1,185 @@
# cfw-auth 注册同步 Autumn 设计
## 背景
`cfw-auth` 当前只暴露 Better Auth 的 `/api/auth/*` 认证面。注册、登录、账号中心、OpenAPI reference 都经由 Better Auth handler 处理Hono 只负责 CORS、Worker `waitUntil` 背景任务承接和请求耗时日志。
本次目标是在用户注册成功后,把用户身份同步到 `coding/autumn`,并让 Autumn 为该用户自动创建 customer、分配免费套餐、开启套餐配置内的试用并持续同步基础资料。认证系统不拥有套餐语义Autumn 仍是计费、权益、试用和套餐规则的唯一 owner。
## 目标
- 邮箱注册成功后自动创建或获取 Autumn customer。
- 注册时把 Better Auth user 的 `id``email``name` 同步到 Autumn。
- 通过 Autumn 的 `autoEnablePlanId` 启用免费套餐。
- 试用由 Autumn plan/product 配置决定,`cfw-auth` 不硬编码试用时长、是否需要卡、价格或权益。
- Autumn 同步失败不阻塞用户注册。
- 保持 `cfw-auth` 的公开 HTTP 边界仍为 `/api/auth/*`,不新增自定义注册 wrapper。
## 非目标
- 不在 `cfw-auth` 实现套餐、价格、权益、usage tracking 或 checkout。
- 不新增独立队列、outbox 表或补偿后台服务。补偿机制可作为后续阶段。
- 不改变 Better Auth schema除非实现阶段发现官方 hook 需要最小字段支持。
- 不修改 `coding/autumn` 的产品配置;免费套餐和试用配置应在 Autumn 侧准备好。
- 不把 Autumn customer 状态写回 `cfw-auth` 用户表作为注册成功条件。
## 推荐方案
使用 Better Auth 的 `databaseHooks.user.create.after` 作为注册成功后的同步触发点,并通过当前 Worker runtime 传入的 `waitUntil` 执行后台任务:
1. Better Auth 成功创建 user。
2. `user.create.after` 收到创建后的 user。
3. hook 构造 Autumn customer payload。
4. 通过 `runtime.waitUntil` 调用 Autumn `customers.getOrCreate`
5. `customers.getOrCreate` 使用 user id 作为稳定 customer id并传入 `autoEnablePlanId`
6. Autumn 返回成功后不改变 Better Auth 响应;失败只记录结构化错误。
这样可以保持认证可用性优先Autumn 或网络短暂故障不会导致用户无法注册。
## 组件
### `src/auth.ts`
继续负责 Better Auth 实例构造。新增的职责只有把注册后 hook 接到 Autumn 同步函数上。
设计要求:
- hook 只处理 user create 后的副作用,不拦截注册输入。
- hook 内不直接展开复杂 fetch 逻辑,而是调用独立模块。
- 如果 `runtime.waitUntil` 存在,使用后台任务;如果没有,则可以直接 await 同步函数,便于测试和非 Worker 场景。
### `src/autumn.ts`
新增 Autumn 同步边界模块。
职责:
- 解析 Autumn 相关 env。
- 判断同步是否启用。
- 将 Better Auth user 映射为 Autumn customer payload。
- 调用 Autumn API。
- 规范化错误日志所需的可公开字段。
该模块不应依赖 Hono context、Better Auth handler 或数据库 adapter。它只接收 `env` 和 user-like 对象,便于单元测试。
### `src/env.ts`
新增显式环境变量类型:
- `AUTUMN_SECRET_KEY?: string`
- `AUTUMN_API_URL?: string`
- `AUTUMN_FREE_PLAN_ID?: string`
- `AUTUMN_REGISTER_SYNC_ENABLED?: string`
同步启用规则:
- `AUTUMN_REGISTER_SYNC_ENABLED=false` 时跳过同步。
- 启用同步时,`AUTUMN_SECRET_KEY``AUTUMN_FREE_PLAN_ID` 必须存在。
- `AUTUMN_API_URL` 可选;未配置时使用 Autumn SDK/API 默认地址。
实现阶段不得为 secret 或 plan id 设置假默认值。
## 数据映射
Better Auth user 到 Autumn customer
```ts
{
customerId: user.id,
email: user.email,
name: user.name,
autoEnablePlanId: env.AUTUMN_FREE_PLAN_ID,
metadata: {
source: "cfw-auth",
authProvider: "better-auth"
}
}
```
`customerId` 使用 Better Auth user id避免邮箱变更导致 Autumn 侧身份漂移。邮箱和姓名作为可更新资料同步给 Autumn。
如果后续需要支持组织计费,应另起 spec把 customer scope 从 `user` 扩展为 `organization``user_and_organization`。本次只做用户级 customer。
## 数据流
```text
POST /api/auth/sign-up/email
-> Hono CORS
-> createAuth(env, runtime)
-> Better Auth handler
-> Better Auth 创建 user
-> databaseHooks.user.create.after
-> syncAutumnCustomerOnRegistration(env, user)
-> Autumn customers.getOrCreate
customerId = Better Auth user id
autoEnablePlanId = configured free plan id
-> Better Auth 原注册响应返回给客户端
```
试用启用不由 `cfw-auth` 直接调用单独 API。Autumn 在 customer 创建并自动启用 plan 时,根据 plan/product 内的 free trial 配置处理试用。
## 错误处理
Autumn 同步失败必须 fail-open
- Better Auth 注册响应不因 Autumn 失败而变为 500。
- 错误日志包含 `event``userId``status``code``message``autumnApiUrlConfigured`
- 错误日志不得包含 `AUTUMN_SECRET_KEY`、Cookie、Authorization header、密码、验证码或完整请求体。
- 缺少 Autumn 必需 env 时,在同步任务中记录配置错误;测试和部署检查应尽早发现该问题。
后续补偿建议:
- 增加一个只读扫描脚本,按 `cfw-auth` 用户表重新调用 `customers.getOrCreate`
- 或新增 outbox/队列,把失败事件持久化并重试。
这些补偿不属于本次 spec 的实现范围。
## 配置与部署
`cfw-auth` 需要配置:
```text
AUTUMN_SECRET_KEY=<Autumn server secret>
AUTUMN_FREE_PLAN_ID=free
AUTUMN_API_URL=<optional local or production Autumn API URL>
AUTUMN_REGISTER_SYNC_ENABLED=true
```
生产环境不允许依赖开发默认值。实现阶段应更新 `TODO.md` 或运维文档,列出这些变量的用途和是否必需。
## 测试
### 单元测试
- env 缺少 `AUTUMN_SECRET_KEY` 时同步函数返回配置错误或记录错误。
- env 缺少 `AUTUMN_FREE_PLAN_ID` 时同步函数不发请求并记录错误。
- `AUTUMN_REGISTER_SYNC_ENABLED=false` 时不发请求。
- payload 使用 `user.id` 作为 `customerId`,并包含 `email``name``autoEnablePlanId`
- Autumn 返回非 2xx 或抛出异常时不向上抛到注册 handler。
### Worker/集成测试
- mock Autumn fetch 后执行 `/api/auth/sign-up/email`,确认注册成功且触发一次 customer 同步。
- mock Autumn 500 后执行注册,确认注册响应仍成功,且错误日志不含 secret。
- 保持现有 `/healthz``/internal/session` 404 测试,防止本次集成扩大公开边界。
### 验收命令
```bash
pnpm typecheck
pnpm test
pnpm ready
```
如果实现阶段引入 `autumn-js` 依赖,还应确认 Worker bundle 能通过 Wrangler 本地启动或 dry-run 检查。
## 风险与取舍
- fail-open 会产生短暂的 Auth 已注册但 Autumn 未同步状态。当前阶段接受这个取舍,因为认证可用性优先。
- 不持久化失败事件意味着自动重试能力有限。后续如果注册量或计费一致性要求提高,应补 outbox/队列。
- 使用 user id 作为 customer id 会让用户级计费简单稳定,但不覆盖组织级 billing。组织级 billing 需要独立设计。
- 如果直接引入 `autumn-js` 增加 Worker bundle 或兼容风险,实现阶段可改用最小 `fetch` 调用 Autumn HTTP API但接口语义仍保持 `getOrCreate``autoEnablePlanId`
## 决策
采用 Better Auth hook + Worker background task + Autumn `customers.getOrCreate` 的方案。注册成功后的 Autumn 同步是异步副作用,不改变 `cfw-auth` 的公开 API 边界,不把套餐和试用规则复制到认证服务。