diff --git a/docs/superpowers/specs/2026-07-02-cfw-auth-autumn-registration-sync-design.md b/docs/superpowers/specs/2026-07-02-cfw-auth-autumn-registration-sync-design.md new file mode 100644 index 0000000..02a329a --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-cfw-auth-autumn-registration-sync-design.md @@ -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_FREE_PLAN_ID=free +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 边界,不把套餐和试用规则复制到认证服务。