# Dramivio 邀请关系兼容性审计

**选型建议：保留 Better Auth 1.7.7 与现有 Email OTP 自动注册，用 Better Auth 的 `databaseHooks.user.create.after` 接入一份独立、很小的 PostgreSQL 邀请关系模块。不要把 `@marinedotsh/better-auth-referral` 0.3.0 直接接入当前 OTP 流程。** 这保留了成熟的认证与 OTP 实现，把邀请码归属、活动规则和奖励账本留在业务域；不需要改 Better Auth OTP 实现或客户端 SDK。

## 候选插件核验

包的真实名称是 `@marinedotsh/better-auth-referral`，最新发布版本为 **0.3.0**，许可证为 **MIT**，要求 `better-auth: ^1.6.23` 和 `zod: ^4.0.0`，Node 最低版本为 18。Better Auth 的 `^1.6.23` 范围包含 1.7.7；我在隔离目录用 `npm install --ignore-scripts` 安装了 1.7.7、Expo 1.7.7 和该插件 0.3.0，没有 peer dependency 冲突。[npm 发布元数据](https://registry.npmjs.org/@marinedotsh%2fbetter-auth-referral)、[仓库 package.json](https://raw.githubusercontent.com/marinedotsh/better-auth-referral/main/package.json)

这是 MIT 开源插件，但仍是 **0.3.0 的 pre-1.0 项目**，不能按成熟、稳定的邀请系统来背书。它的源码只在 `/sign-up/email` 和 OAuth 注册路径读取邀请码、创建关系；没有匹配 `/sign-in/email-otp` 的 hook。[插件 hook 源码](https://raw.githubusercontent.com/marinedotsh/better-auth-referral/main/src/server.ts) Better Auth Email OTP 在 `/sign-in/email-otp` 验证 OTP 后，直接为尚不存在的邮箱创建已验证用户并建立 session，因此当前被 `/sign-up/email` hook 覆盖不到。[Better Auth Email OTP 文档](https://better-auth.com/docs/plugins/email-otp)

插件的关系表对 `referredUserId` 设置了数据库唯一约束，因此数据库能拒绝同一用户的重复关系；验证码表对 code 和 userId 也有唯一约束。[插件 schema](https://raw.githubusercontent.com/marinedotsh/better-auth-referral/main/src/schema.ts) 但关系创建逻辑是先查再插入，插入关系时没有捕获唯一冲突。并发重复创建会被 PostgreSQL 拦下，但插件没有把该冲突处理成幂等成功。[插件关系创建逻辑](https://raw.githubusercontent.com/marinedotsh/better-auth-referral/main/src/server.ts)

客户端不能调用 `complete_step`。插件只把 step API 暴露为 Better Auth 的服务端方法；源码调用 `createAuthEndpoint.serverOnly`，没有可供客户端请求的 HTTP 路径。仓库 README 的 endpoint 表列出了一个 HTTP 路径，但与实现不符；应以服务端源码为准。[step API 实现](https://raw.githubusercontent.com/marinedotsh/better-auth-referral/main/src/routes/mark-referral-step-complete.ts) 若未来采用该 API，只能由受信任的服务端、任务或 webhook 调用 `auth.api.markReferralStepComplete(...)`。

插件的 step 配置是静态字符串列表，不能管理后台要求的每个活动目标集数、有效观看时长、倍速、奖励和有效期。即使外挂 OTP 关系适配器，也会重复实现插件内部的关系创建逻辑；本次不建议把这套业务关系建立在插件的私有实现上。它的 `sign_up` step 会自动记为完成；这只代表插件的 step 状态，不应连接奖励发放。插件的完成回调也不是 PostgreSQL 奖励账本。

## 本地验证

PoC 使用 Node 22.16、Better Auth 1.7.7、`@better-auth/expo` 1.7.7、插件 0.3.0 和现有 `tests/postgres.mjs` 启动的 PostgreSQL 18 临时容器。请求直接调用 Better Auth handler，没有打开固定端口；OTP 只写入本地内存数组，没有外发邮件。PoC 在 `finally` 中关闭数据库池并停止容器；结束后确认没有留下测试容器。

在当前 OTP 配置下，带有效邀请码的新邮箱登录返回 200、关系数为 0；带无效邀请码的新邮箱登录也返回 200、关系数为 0；既有邮箱再次登录时带有效邀请码仍返回 200、用户 ID 不变且没有新增关系。这表示插件既不会在新邮箱 OTP 注册时建立关系，也不会误绑既有邮箱；邀请码在这条路由上被忽略，所以无效邀请码不会阻断认证。

我还在同一 PoC 给 Better Auth 配置了 `databaseHooks.user.create.after`：新邮箱 OTP 创建用户时，hook 收到了 `ctx.path === "/sign-in/email-otp"`、自定义 body 中的 `inviteCode` 以及 `x-referral-code` header。既有用户第二次 OTP 登录没有再次触发用户创建 hook。Better Auth 文档支持此 database hook；实际 1.7.7 PoC 也验证了它能拿到本请求上下文。[Better Auth database hooks 文档](https://better-auth.com/docs/concepts/database)

对插件迁移出的 PostgreSQL schema 直接重复插入同一 `referredUserId`，第二次写入返回 SQLSTATE `23505`，确认约束在数据库层有效。这个检查验证的是数据库唯一性；它没有证明插件自己的关系插入在并发竞争时会无错误返回。没有运行插件仓库测试套件，也没有接入真实数据库、邮箱服务或生产代码。

## I00 接入要点

- 在 `createConsumerAuth` 的 Better Auth 配置中增加 `databaseHooks.user.create.after(user, ctx)`。只处理 `ctx.path === "/sign-in/email-otp"` 的新用户创建事件，从 `ctx.body.inviteCode` 或 `ctx.request.headers.get("x-referral-code")` 读取邀请码。Email OTP 1.7.7 的 body schema 接受附加字段；本次通过实际请求确认字段会到达 hook，不需要改 OTP 实现或 SDK。
- 在 hook 中查邀请码所属用户、活动和 `expires_at`。格式错误、未知或过期邀请码只是不建立邀请关系，不能抛错阻止已通过 OTP 的登录。hook 只随真实 user create 运行，既有邮箱的 OTP 登录不会获得新的邀请归属；不要为同 IP 用户设置唯一或一刀切拒绝规则，NAT 共享 IP 只作风险信号。
- 邀请关系用独立 PG 表记录活动、referrer、referred、来源 code 和 `pending` 状态；对 `referred_user_id` 建唯一约束，插入采用 `ON CONFLICT (referred_user_id) DO NOTHING`，使重复事件幂等。活动表保存后台可调的集数、时长、倍速、奖励和有效期。邀请码生成也要用数据库唯一约束处理碰撞。
- 奖励继续由独立 PG 业务账本保存，并在服务端核对活动规则和可信播放事件后记账；注册事件只创建 `pending` 关系。此审计没有验证 H00watch 的观看估算或反刷能力，不应把客户端 position 或 H00watch 估算当作奖励 proof；也没有替你构造额外观看证明。
- Better Auth 官方说明 database `after` hook 在实体创建后执行；关系写入应设计可重试和可观测，且不要让关系存储的无效码或可恢复故障把 OTP 登录变成失败。[Better Auth database hooks 文档](https://better-auth.com/docs/concepts/database)

本次没有修改平台业务代码、部署服务、连接生产数据库、发送邮件，或生成新的 hash、baseline、gate。
