feat: add better authentication features and organization management

- Introduced new database migration for enhanced user and organization management.
- Updated package dependencies to include new Better Auth modules for API keys, Expo, and i18n.
- Implemented SMS functionality for phone verification and password resets.
- Enhanced authentication plugins with username and phone number support.
- Added performance configuration options for session cookie caching and API key updates.
- Updated email templates to include organization invitation messages.
- Improved testing coverage for new features and configurations.
This commit is contained in:
2026-06-10 19:39:15 -07:00
parent e28267fa4e
commit e3057928a2
21 changed files with 2200 additions and 36 deletions

View File

@@ -0,0 +1,775 @@
# Better Auth 接口响应时间优化实现计划
> **给 agent 执行者:** 必选子技能:使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans` 按任务逐项实现本计划。步骤统一使用 checkbox`- [ ]`)语法跟踪。
**目标:**`cfw-auth` 的 Better Auth 热路径加入低风险性能配置和安全的请求耗时观测。
**方案概览:** 先抽出性能配置解析函数,再把 session cookie cache 和 API key `deferUpdates` 接入 Better Auth 配置。随后在 Worker 入口增加不改写响应的结构化观测包装,补测试和运维文档,最后验证 migration、typecheck、测试全部通过。
**技术栈:** Cloudflare Workers、Hono、Better Auth、D1、TypeScript、Vitest、Wrangler
---
## 文件结构
- 新建:`src/performance.ts`
- 负责解析性能相关 envsession cookie cache TTL、API key defer 开关。
- 不依赖 Better Auth、Hono 或 Worker 请求对象,便于单元测试。
- 新建:`src/observability.ts`
- 负责 `/api/auth/*` 请求的耗时观测。
- 提供 `withAuthRequestLogging(request, handler)`,只包装 Response不读取请求体不记录敏感 header。
- 修改:`src/env.ts`
-`Env` 接口中加入 `BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE``API_KEY_DEFER_UPDATES`
- 修改:`src/auth.ts`
- 使用 `authPerformanceConfig(env)` 设置 Better Auth `session.cookieCache`
- 继续保留现有 `advanced.backgroundTasks.handler`
- 修改:`src/plugins.ts`
- 使用 `authPerformanceConfig(env).apiKeyDeferUpdates` 为两个 `apiKey` config 设置 `deferUpdates`
- 修改:`src/index.ts`
-`/api/auth/*` handler 中使用 `withAuthRequestLogging(...)` 包裹 Better Auth handler。
- 修改:`wrangler.jsonc`
- 增加非 secret 默认变量。
- 增加 Cloudflare `observability` 配置。
- 不强制增加 `placement.mode = "smart"`
- 修改:`tests/auth-config.test.ts`
- 覆盖性能配置解析、Better Auth session cookie cache、API key defer 开关。
- 修改:`tests/auth-worker.test.ts`
- 覆盖观测日志字段、敏感字段过滤、响应不被改写。
- 修改:`docs/auth-operations.md`
- 记录性能配置、观测、生产验证和后续 Smart Placement 决策门槛。
## 注意事项
- 当前工作树已经有账号中心插件相关未提交改动。执行本计划时不要回退那些改动。
- 性能配置不应改变 Better Auth schema`pnpm db:check` 应保持 migration up to date。
- Better Auth 1.6.x 的 `session.cookieCache` 支持 `enabled``maxAge`。不要启用 `refreshCache`,它更适合 stateless/DB-less 场景。
- `@better-auth/api-key``deferUpdates` 要求主 Better Auth 配置存在 `advanced.backgroundTasks.handler`。当前 `src/auth.ts` 已通过 Worker `waitUntil` 提供该 handler。
### 任务 1抽出性能配置解析
**文件:**
- 新建:`src/performance.ts`
- 修改:`src/env.ts`
- 测试:`tests/auth-config.test.ts`
- [ ] **步骤 1先写失败测试**
`tests/auth-config.test.ts` 的 import 中加入:
```ts
import { authPerformanceConfig } from "../src/performance";
```
`auth env helpers` describe 后追加:
```ts
describe("auth performance config", () => {
it("uses conservative defaults", () => {
expect(authPerformanceConfig(env)).toEqual({
sessionCookieCacheMaxAge: 300,
apiKeyDeferUpdates: true,
});
});
it("parses session cookie cache max age from env", () => {
expect(
authPerformanceConfig({
...env,
BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "60",
}).sessionCookieCacheMaxAge,
).toBe(60);
});
it("falls back for invalid session cookie cache max age", () => {
expect(
authPerformanceConfig({
...env,
BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "0",
}).sessionCookieCacheMaxAge,
).toBe(300);
expect(
authPerformanceConfig({
...env,
BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "not-a-number",
}).sessionCookieCacheMaxAge,
).toBe(300);
});
it("allows disabling api key deferred updates", () => {
expect(authPerformanceConfig({ ...env, API_KEY_DEFER_UPDATES: "false" })).toMatchObject({
apiKeyDeferUpdates: false,
});
expect(authPerformanceConfig({ ...env, API_KEY_DEFER_UPDATES: "true" })).toMatchObject({
apiKeyDeferUpdates: true,
});
});
});
```
- [ ] **步骤 2运行测试确认它先失败**
运行:
```bash
pnpm vitest run tests/auth-config.test.ts
```
预期FAIL错误包含 `Failed to resolve import "../src/performance"``Cannot find module '../src/performance'`
- [ ] **步骤 3编写最小实现**
`src/env.ts``Env` interface 中加入:
```ts
BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE?: string;
API_KEY_DEFER_UPDATES?: string;
```
新建 `src/performance.ts`
```ts
import { type Env } from "./env";
export interface AuthPerformanceConfig {
sessionCookieCacheMaxAge: number;
apiKeyDeferUpdates: boolean;
}
const defaultSessionCookieCacheMaxAge = 300;
export function authPerformanceConfig(env: Env): AuthPerformanceConfig {
return {
sessionCookieCacheMaxAge: positiveIntegerEnv(
env.BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE,
defaultSessionCookieCacheMaxAge,
),
apiKeyDeferUpdates: env.API_KEY_DEFER_UPDATES !== "false",
};
}
function positiveIntegerEnv(value: string | undefined, fallback: number): number {
if (!value) {
return fallback;
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
return fallback;
}
return parsed;
}
```
- [ ] **步骤 4再次运行测试确认通过**
运行:
```bash
pnpm vitest run tests/auth-config.test.ts
```
预期PASS。
- [ ] **步骤 5提交**
```bash
git add src/env.ts src/performance.ts tests/auth-config.test.ts
git commit -m "feat: add auth performance config"
```
### 任务 2接入 session cookie cache
**文件:**
- 修改:`src/auth.ts`
- 测试:`tests/auth-config.test.ts`
- [ ] **步骤 1先写失败测试**
`tests/auth-config.test.ts` 顶部加入:
```ts
import { createAuth } from "../src/auth";
```
`auth plugins` describe 后追加:
```ts
describe("auth runtime performance options", () => {
it("enables session cookie cache with configured max age", () => {
const auth = createAuth({
BETTER_AUTH_URL: "http://localhost:8788",
TRUSTED_ORIGINS: "http://localhost:8787",
BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "60",
});
expect(auth.options.session?.cookieCache).toEqual({
enabled: true,
maxAge: 60,
});
});
});
```
- [ ] **步骤 2运行测试确认它先失败**
运行:
```bash
pnpm vitest run tests/auth-config.test.ts
```
预期FAIL断言显示 `auth.options.session?.cookieCache``undefined`
- [ ] **步骤 3编写最小实现**
`src/auth.ts` 中加入 import
```ts
import { authPerformanceConfig } from "./performance";
```
`createAuth` 函数开头加入:
```ts
const performance = authPerformanceConfig(env);
```
`betterAuth({ ... })` 配置中,放在 `trustedOrigins` 后面加入:
```ts
session: {
cookieCache: {
enabled: true,
maxAge: performance.sessionCookieCacheMaxAge,
},
},
```
完整目标形状:
```ts
export function createAuth(env: Env, runtime: AuthRuntime = {}) {
const performance = authPerformanceConfig(env);
return betterAuth({
database: env.DB,
secret: env.BETTER_AUTH_SECRET ?? "development-secret-change-before-production",
baseURL: env.BETTER_AUTH_URL,
trustedOrigins: trustedOrigins(env),
session: {
cookieCache: {
enabled: true,
maxAge: performance.sessionCookieCacheMaxAge,
},
},
socialProviders: createSocialProviders(env),
// keep existing options unchanged
});
}
```
- [ ] **步骤 4再次运行测试确认通过**
运行:
```bash
pnpm vitest run tests/auth-config.test.ts
```
预期PASS。
- [ ] **步骤 5确认 migration 没有被性能配置影响**
运行:
```bash
pnpm db:check
```
预期PASS并输出 committed migration is up to date。
- [ ] **步骤 6提交**
```bash
git add src/auth.ts tests/auth-config.test.ts
git commit -m "feat: enable auth session cookie cache"
```
### 任务 3接入 API key 延迟更新开关
**文件:**
- 修改:`src/plugins.ts`
- 测试:`tests/auth-config.test.ts`
- [ ] **步骤 1先写失败测试**
`tests/auth-config.test.ts``auth plugins` describe 中追加:
```ts
it("enables api key deferred updates by default", () => {
const plugins = createAuthPlugins({
BETTER_AUTH_URL: "http://localhost:8788",
TRUSTED_ORIGINS: "http://localhost:8787",
});
const apiKeyPlugin = plugins.find((plugin) => plugin.id === "api-key");
expect(apiKeyPlugin?.options).toEqual(
expect.arrayContaining([
expect.objectContaining({
configId: "default",
deferUpdates: true,
}),
expect.objectContaining({
configId: "organization",
deferUpdates: true,
}),
]),
);
});
it("allows disabling api key deferred updates", () => {
const plugins = createAuthPlugins({
BETTER_AUTH_URL: "http://localhost:8788",
TRUSTED_ORIGINS: "http://localhost:8787",
API_KEY_DEFER_UPDATES: "false",
});
const apiKeyPlugin = plugins.find((plugin) => plugin.id === "api-key");
expect(apiKeyPlugin?.options).toEqual(
expect.arrayContaining([
expect.objectContaining({
configId: "default",
deferUpdates: false,
}),
expect.objectContaining({
configId: "organization",
deferUpdates: false,
}),
]),
);
});
```
- [ ] **步骤 2运行测试确认它先失败**
运行:
```bash
pnpm vitest run tests/auth-config.test.ts
```
预期FAIL`deferUpdates``undefined`
- [ ] **步骤 3编写最小实现**
`src/plugins.ts` 中加入 import
```ts
import { authPerformanceConfig } from "./performance";
```
`createAuthPlugins` 函数开头加入:
```ts
const performance = authPerformanceConfig(env);
```
在两个 `apiKey` 配置对象中都加入:
```ts
deferUpdates: performance.apiKeyDeferUpdates,
```
目标形状示例:
```ts
{
configId: "default",
defaultPrefix: "cfw_",
requireName: true,
enableMetadata: true,
deferUpdates: performance.apiKeyDeferUpdates,
rateLimit: {
enabled: true,
timeWindow: 1_000 * 60 * 60 * 24,
maxRequests: 1_000,
},
},
```
- [ ] **步骤 4再次运行测试确认通过**
运行:
```bash
pnpm vitest run tests/auth-config.test.ts
```
预期PASS。
- [ ] **步骤 5提交**
```bash
git add src/plugins.ts tests/auth-config.test.ts
git commit -m "feat: defer api key counter updates"
```
### 任务 4新增认证请求观测包装
**文件:**
- 新建:`src/observability.ts`
- 修改:`src/index.ts`
- 测试:`tests/auth-worker.test.ts`
- [ ] **步骤 1先写失败测试**
`tests/auth-worker.test.ts` import 中改为:
```ts
import { afterEach, describe, expect, it, vi } from "vitest";
```
`const env` 后加入:
```ts
afterEach(() => {
vi.restoreAllMocks();
});
```
`cfw-auth worker` describe 中追加:
```ts
it("logs auth request timing without changing the response", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const response = await worker.fetch(
new Request("http://auth.local/api/auth/reference", {
headers: {
"cf-ray": "test-ray",
Authorization: "Bearer secret-token",
Cookie: "better-auth.session_token=secret-cookie",
},
}),
env,
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type") ?? "").toContain("text/html");
expect(log).toHaveBeenCalledTimes(1);
const [rawMessage] = log.mock.calls[0] ?? [];
expect(typeof rawMessage).toBe("string");
const message = String(rawMessage);
const event = JSON.parse(message) as Record<string, unknown>;
expect(event).toMatchObject({
event: "auth_request",
method: "GET",
path: "/api/auth/reference",
status: 200,
cfRay: "test-ray",
});
expect(typeof event.durationMs).toBe("number");
expect(message).not.toContain("secret-token");
expect(message).not.toContain("secret-cookie");
expect(message.toLowerCase()).not.toContain("authorization");
expect(message.toLowerCase()).not.toContain("cookie");
});
```
- [ ] **步骤 2运行测试确认它先失败**
运行:
```bash
pnpm vitest run tests/auth-worker.test.ts
```
预期FAIL`console.log` 没有被调用。
- [ ] **步骤 3编写最小实现**
新建 `src/observability.ts`
```ts
export type AuthRequestHandler = () => Response | Promise<Response>;
export async function withAuthRequestLogging(
request: Request,
handler: AuthRequestHandler,
): Promise<Response> {
const startedAt = Date.now();
let response: Response;
try {
response = await handler();
return response;
} finally {
const status = response?.status ?? 500;
logAuthRequest(request, status, Date.now() - startedAt);
}
}
function logAuthRequest(request: Request, status: number, durationMs: number): void {
try {
const event = {
event: "auth_request",
method: request.method,
path: new URL(request.url).pathname,
status,
durationMs,
colo: request.cf?.colo,
cfRay: request.headers.get("cf-ray") ?? undefined,
};
console.log(JSON.stringify(event));
} catch {
// Observability must not affect auth responses.
}
}
```
修改 `src/index.ts`,加入 import
```ts
import { withAuthRequestLogging } from "./observability";
```
把现有 auth handler
```ts
app.on(["POST", "GET"], "/api/auth/*", (c) => {
const auth = createAuth(c.env, {
waitUntil: (promise) => {
c.executionCtx.waitUntil(promise);
},
});
return auth.handler(c.req.raw);
});
```
改成:
```ts
app.on(["POST", "GET"], "/api/auth/*", (c) => {
const auth = createAuth(c.env, {
waitUntil: (promise) => {
c.executionCtx.waitUntil(promise);
},
});
return withAuthRequestLogging(c.req.raw, () => auth.handler(c.req.raw));
});
```
- [ ] **步骤 4再次运行测试确认通过**
运行:
```bash
pnpm vitest run tests/auth-worker.test.ts
```
预期PASS。
- [ ] **步骤 5提交**
```bash
git add src/index.ts src/observability.ts tests/auth-worker.test.ts
git commit -m "feat: log auth request latency"
```
### 任务 5补充 Wrangler 和运维文档
**文件:**
- 修改:`wrangler.jsonc`
- 修改:`docs/auth-operations.md`
- [ ] **步骤 1更新 Wrangler 配置**
`wrangler.jsonc``vars` 中加入:
```jsonc
"BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE": "300",
"API_KEY_DEFER_UPDATES": "true"
```
在顶层加入:
```jsonc
"observability": {
"enabled": true,
"head_sampling_rate": 0.1
},
```
目标结构示例:
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "cfw-auth",
"main": "src/index.ts",
"compatibility_date": "2026-06-10",
"compatibility_flags": [
"nodejs_compat"
],
"observability": {
"enabled": true,
"head_sampling_rate": 0.1
},
"vars": {
"BETTER_AUTH_URL": "http://localhost:8788",
"TRUSTED_ORIGINS": "http://localhost:8787",
"MAIL_PROVIDER": "resend",
"MAIL_FROM": "noreply@example.com",
"CAPTCHA_PROVIDER": "cloudflare-turnstile",
"BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE": "300",
"API_KEY_DEFER_UPDATES": "true"
},
"d1_databases": [
{
"binding": "DB",
"database_name": "cfw-auth",
"database_id": "90fe25a4-9d22-43a6-9ade-3a15afc3ab47"
}
]
}
```
- [ ] **步骤 2更新运维文档**
`docs/auth-operations.md``Configuration` 小节中,追加:
```md
Performance-related non-secret defaults live in `wrangler.jsonc`:
- `BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE`: Better Auth session cookie cache TTL in seconds. Default is `300`. Lower it if session revocation or role changes must propagate faster.
- `API_KEY_DEFER_UPDATES`: When `true`, API key request counters and timestamps are deferred through Better Auth background tasks and Worker `waitUntil`.
Auth request latency is logged as structured JSON with:
- `event=auth_request`
- `method`
- `path`
- `status`
- `durationMs`
- optional `colo`
- optional `cfRay`
Do not log request bodies, cookies, authorization headers, API keys, emails, phone numbers, or OTP codes.
```
`Deployment` 小节后追加:
```md
## Performance Validation
After deployment, compare p50, p95, and p99 for:
- `/api/auth/get-session`
- `/api/auth/api-key/verify`
- phone OTP endpoints
- organization invitation endpoints
Enable Cloudflare Smart Placement only after logs show that latency is dominated by D1 or external provider round trips. Evaluate D1 read replication or secondary storage only after the first-stage cache and deferred-update changes are measured in production.
```
- [ ] **步骤 3验证配置 schema 和文档改动**
运行:
```bash
pnpm typecheck
```
预期PASS。
- [ ] **步骤 4提交**
```bash
git add wrangler.jsonc docs/auth-operations.md
git commit -m "docs: document auth performance operations"
```
### 任务 6全量验证和最终提交检查
**文件:**
- 修改:无新文件,验证全项目状态。
- [ ] **步骤 1运行 migration 检查**
运行:
```bash
pnpm db:check
```
预期PASS并显示 migration up to date。若失败且 diff 只来自性能配置,说明实现错误;性能配置不应影响 schema。
- [ ] **步骤 2运行类型检查**
运行:
```bash
pnpm typecheck
```
预期PASS。
- [ ] **步骤 3运行测试**
运行:
```bash
pnpm test
```
预期PASS。
- [ ] **步骤 4查看最终 diff**
运行:
```bash
git status --short
git diff -- src/performance.ts src/observability.ts src/env.ts src/auth.ts src/plugins.ts src/index.ts tests/auth-config.test.ts tests/auth-worker.test.ts wrangler.jsonc docs/auth-operations.md
```
预期:只看到本计划相关文件改动。不要回退或改动当前工作树里的其它账号中心文件。
- [ ] **步骤 5提交最终验证记录**
如果前面每个任务都已提交,且这一步没有新增文件改动,则不需要创建空提交。若本任务中修正了测试或文档小问题,则提交:
```bash
git add src/performance.ts src/observability.ts src/env.ts src/auth.ts src/plugins.ts src/index.ts tests/auth-config.test.ts tests/auth-worker.test.ts wrangler.jsonc docs/auth-operations.md
git commit -m "test: verify auth performance optimization"
```
## Spec 覆盖自检
- session cookie cache任务 1、任务 2 覆盖。
- API key `deferUpdates`:任务 1、任务 3 覆盖。
- `/api/auth/*` 结构化耗时日志:任务 4 覆盖。
- 敏感字段不记录:任务 4 覆盖。
- 配置可关闭或调低:任务 1、任务 3、任务 5 覆盖。
- 不改变迁移:任务 2、任务 6 覆盖。
- 运维与生产验证说明:任务 5 覆盖。
## 计划自检结论
- 无未完成标记或空白段落。
- 每个代码改动任务都有先失败测试、最小实现、验证命令和提交步骤。
- 类型、函数名和配置名在所有任务中保持一致。
- 计划只覆盖第一阶段低风险性能优化,没有混入存储层重构或 Smart Placement 强制启用。