diff --git a/docs/superpowers/plans/2026-06-10-cfw-auth-account-center.md b/docs/superpowers/plans/2026-06-10-cfw-auth-account-center.md new file mode 100644 index 0000000..f68662d --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-cfw-auth-account-center.md @@ -0,0 +1,1340 @@ +# cfw-auth 完整账号中心实现计划 + +> **给 agent 执行者:** 必选子技能:使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans` 按任务逐项实现本计划。步骤统一使用 checkbox(`- [ ]`)语法跟踪。 + +**目标:** 把 `cfw-auth` 从基础邮箱密码登录 Worker 升级为完整账号中心,覆盖注册、登录、邮箱验证、找回密码、Email OTP、OAuth、Passkey、2FA、多会话、管理能力和 OpenAPI。 + +**方案概览:** 继续保留 Hono + Better Auth + D1 的现有入口形态,把账号能力集中在 `src/auth.ts` 的 `createAuth(env)` 组合根中,通过小型 env/helper/message adapter 文件降低配置复杂度。按阶段启用 Better Auth 核心配置和插件,先建立可测试的邮件与 OpenAPI 骨架,再逐步接入验证码、OTP、OAuth、Passkey 独立包、安全插件和 D1 migration。 + +**技术栈:** Cloudflare Workers、Hono、Better Auth 1.6.x、D1、TypeScript、Vitest、Wrangler、pnpm。 + +--- + +## 执行前检查 + +- 当前工作区可能已有未提交源码改动。执行前先运行 `git status --short`,确认这些改动是否属于本计划。如果不是本计划产生的改动,只在其基础上追加必要修改,不要回滚。 +- 每个任务结束后运行任务内列出的测试命令;通过后按任务提交。若 pre-commit 或 formatter 改写文件,提交后再次运行 `git status --short`。 +- Better Auth 插件会改变 endpoint、schema、hook 和类型。每启用一组插件后都要执行 typecheck 和至少一条真实 Worker fetch 测试。 + +## 文件结构 + +- 修改:`src/auth.ts`。Better Auth 组合根,导入并配置核心选项与插件。 +- 修改:`src/env.ts`。Worker `Env` 类型、CSV/boolean 解析、OAuth/captcha/passkey 配置读取。 +- 新建:`src/email.ts`。邮件消息类型与 `sendEmail(env, message)` 适配器。 +- 新建:`src/oauth.ts`。从 env 读取通用 OAuth providers。 +- 新建:`src/plugins.ts`。按 env 组合 Better Auth 插件,避免 `auth.ts` 过大。 +- 新建:`src/password.ts`。密码策略和泄露密码插件配置常量。 +- 修改:`src/index.ts`。保留 `/api/auth/*` 的 GET/POST handler 和 CORS,不新增自定义 session wrapper,也不恢复自定义 health endpoint。 +- 修改:`tests/auth-worker.test.ts`。Worker 路由、CORS、OpenAPI 和配置缺失测试。 +- 新建:`tests/auth-config.test.ts`。env 解析、OAuth provider 构造、邮件消息构造测试。 +- 新建:`migrations/0001_better_auth_account_center.sql`。第一轮 D1 schema 迁移,由任务 11 使用 Better Auth CLI 生成并审查。 +- 修改:`wrangler.jsonc`。补齐开发环境变量示例。 +- 修改:`package.json` / `pnpm-lock.yaml`。增加 `@better-auth/passkey`,后续如启用 API Key 再增加 `@better-auth/api-key`。 + +--- + +### 任务 1:保护现有 Worker 入口行为 + +**文件:** +- 修改:`tests/auth-worker.test.ts` +- 修改:`src/index.ts` + +- [ ] **步骤 1:先写 Worker 入口回归测试** + +在 `tests/auth-worker.test.ts` 中保留现有测试,并加入: + +```ts +it("routes Better Auth traffic through /api/auth/*", async () => { + const response = await worker.fetch(new Request("http://auth.local/api/auth/reference"), env); + expect([200, 404, 500]).toContain(response.status); +}); + +it("applies configured CORS origin for auth routes", async () => { + const response = await worker.fetch( + new Request("http://auth.local/api/auth/reference", { + headers: { + Origin: "http://localhost:8787", + }, + }), + env, + ); + expect(response.headers.get("access-control-allow-origin")).toBe("http://localhost:8787"); +}); +``` + +- [ ] **步骤 2:运行测试,确认当前行为** + +运行:`pnpm test -- tests/auth-worker.test.ts` + +预期:如果 `/api/auth/reference` 尚未启用,第一条测试允许 `404`;CORS 测试应通过。如失败,先修复 `src/index.ts` 中 `/api/auth/*` 的 CORS 和 handler 顺序。 + +- [ ] **步骤 3:确认 `src/index.ts` 只做入口职责** + +`src/index.ts` 应保持这个结构: + +```ts +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { createAuth } from "./auth"; +import { trustedOrigins, type Env } from "./env"; + +const app = new Hono<{ Bindings: Env }>(); + +app.use( + "/api/auth/*", + cors({ + origin: (origin, c) => { + const allowed = trustedOrigins(c.env); + return origin && allowed.includes(origin) ? origin : allowed[0] ?? origin; + }, + allowHeaders: ["Content-Type", "Authorization"], + allowMethods: ["GET", "POST", "OPTIONS"], + credentials: true, + }), +); + +app.on(["POST", "GET"], "/api/auth/*", (c) => { + const auth = createAuth(c.env); + return auth.handler(c.req.raw); +}); + +export default app; +``` + +- [ ] **步骤 4:再次运行测试** + +运行:`pnpm test -- tests/auth-worker.test.ts` + +预期:PASS。 + +- [ ] **步骤 5:提交** + +```bash +git add src/index.ts tests/auth-worker.test.ts +git commit -m "test: lock auth worker routing" +``` + +--- + +### 任务 2:补齐 Env 解析与配置测试 + +**文件:** +- 修改:`src/env.ts` +- 新建:`tests/auth-config.test.ts` + +- [ ] **步骤 1:先写 env 解析测试** + +新建 `tests/auth-config.test.ts`: + +```ts +import { describe, expect, it } from "vitest"; +import { + booleanEnv, + csvEnv, + optionalEnv, + requiredEnv, + trustedOrigins, + type Env, +} from "../src/env"; + +const env: Env = { + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: " http://localhost:8787, https://app.example.com ", +}; + +describe("auth env helpers", () => { + it("parses trusted origins from comma-separated env", () => { + expect(trustedOrigins(env)).toEqual(["http://localhost:8787", "https://app.example.com"]); + }); + + it("parses optional csv values", () => { + expect(csvEnv(" google, github ,, ")).toEqual(["google", "github"]); + }); + + it("parses boolean env values", () => { + expect(booleanEnv("true")).toBe(true); + expect(booleanEnv("1")).toBe(true); + expect(booleanEnv("false")).toBe(false); + expect(booleanEnv(undefined)).toBe(false); + }); + + it("throws for missing required env", () => { + expect(() => requiredEnv({}, "MAIL_FROM")).toThrow("Missing required env: MAIL_FROM"); + }); + + it("returns optional env values", () => { + expect(optionalEnv({ MAIL_FROM: "noreply@example.com" }, "MAIL_FROM")).toBe("noreply@example.com"); + expect(optionalEnv({}, "MAIL_FROM")).toBeUndefined(); + }); +}); +``` + +- [ ] **步骤 2:运行测试,确认失败** + +运行:`pnpm test -- tests/auth-config.test.ts` + +预期:FAIL,提示 `booleanEnv`、`csvEnv`、`requiredEnv` 或 `optionalEnv` 未导出。 + +- [ ] **步骤 3:实现 env helper** + +把 `src/env.ts` 改为: + +```ts +export interface Env { + DB?: D1Database; + BETTER_AUTH_SECRET?: string; + BETTER_AUTH_URL: string; + TRUSTED_ORIGINS: string; + MAIL_FROM?: string; + MAIL_PROVIDER?: string; + RESEND_API_KEY?: string; + CAPTCHA_PROVIDER?: string; + CAPTCHA_SECRET_KEY?: string; + OAUTH_PROVIDERS?: string; + GOOGLE_CLIENT_ID?: string; + GOOGLE_CLIENT_SECRET?: string; + GITHUB_CLIENT_ID?: string; + GITHUB_CLIENT_SECRET?: string; + GENERIC_OAUTH_PROVIDER_ID?: string; + GENERIC_OAUTH_DISCOVERY_URL?: string; + GENERIC_OAUTH_CLIENT_ID?: string; + GENERIC_OAUTH_CLIENT_SECRET?: string; + PASSKEY_RP_ID?: string; + PASSKEY_RP_NAME?: string; + PASSKEY_ORIGIN?: string; + ENABLE_JWT?: string; + ENABLE_BEARER?: string; +} + +export function csvEnv(value: string | undefined): string[] { + return (value ?? "") + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + +export function booleanEnv(value: string | undefined): boolean { + return value === "true" || value === "1"; +} + +export function requiredEnv(source: Record, key: string): string { + const value = source[key]; + if (!value) { + throw new Error(`Missing required env: ${key}`); + } + return value; +} + +export function optionalEnv(source: Record, key: string): string | undefined { + return source[key] || undefined; +} + +export function trustedOrigins(env: Env): string[] { + return csvEnv(env.TRUSTED_ORIGINS); +} +``` + +- [ ] **步骤 4:再次运行测试和 typecheck** + +运行: + +```bash +pnpm test -- tests/auth-config.test.ts +pnpm typecheck +``` + +预期:PASS。 + +- [ ] **步骤 5:提交** + +```bash +git add src/env.ts tests/auth-config.test.ts +git commit -m "feat: add auth env helpers" +``` + +--- + +### 任务 3:建立邮件适配器接口 + +**文件:** +- 新建:`src/email.ts` +- 修改:`tests/auth-config.test.ts` + +- [ ] **步骤 1:先写邮件消息测试** + +在 `tests/auth-config.test.ts` 追加: + +```ts +import { buildAuthEmail, sendEmail } from "../src/email"; + +describe("auth email adapter", () => { + it("builds a verification email", () => { + const message = buildAuthEmail({ + kind: "verify-email", + to: "user@example.com", + url: "https://auth.example.com/verify", + }); + expect(message.subject).toBe("Verify your email"); + expect(message.to).toBe("user@example.com"); + expect(message.text).toContain("https://auth.example.com/verify"); + }); + + it("fails clearly when no mail provider is configured", async () => { + await expect( + sendEmail( + { + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + }, + { + to: "user@example.com", + subject: "Test", + text: "Test", + }, + ), + ).rejects.toThrow("MAIL_PROVIDER is not configured"); + }); +}); +``` + +- [ ] **步骤 2:运行测试,确认失败** + +运行:`pnpm test -- tests/auth-config.test.ts` + +预期:FAIL,提示 `../src/email` 不存在。 + +- [ ] **步骤 3:实现邮件适配器** + +新建 `src/email.ts`: + +```ts +import type { Env } from "./env"; + +export type AuthEmailKind = "verify-email" | "reset-password" | "email-otp" | "magic-link"; + +export interface AuthEmailInput { + kind: AuthEmailKind; + to: string; + url?: string; + otp?: string; +} + +export interface EmailMessage { + to: string; + subject: string; + text: string; +} + +export function buildAuthEmail(input: AuthEmailInput): EmailMessage { + if (input.kind === "email-otp") { + return { + to: input.to, + subject: "Your sign-in code", + text: `Your sign-in code is ${input.otp ?? ""}.`, + }; + } + + if (input.kind === "reset-password") { + return { + to: input.to, + subject: "Reset your password", + text: `Use this link to reset your password: ${input.url ?? ""}`, + }; + } + + if (input.kind === "magic-link") { + return { + to: input.to, + subject: "Sign in to your account", + text: `Use this link to sign in: ${input.url ?? ""}`, + }; + } + + return { + to: input.to, + subject: "Verify your email", + text: `Use this link to verify your email: ${input.url ?? ""}`, + }; +} + +export async function sendEmail(env: Env, message: EmailMessage): Promise { + if (!env.MAIL_PROVIDER) { + throw new Error("MAIL_PROVIDER is not configured"); + } + + if (env.MAIL_PROVIDER !== "resend") { + throw new Error(`Unsupported MAIL_PROVIDER: ${env.MAIL_PROVIDER}`); + } + + if (!env.RESEND_API_KEY || !env.MAIL_FROM) { + throw new Error("RESEND_API_KEY and MAIL_FROM are required for resend"); + } + + const response = await fetch("https://api.resend.com/emails", { + method: "POST", + headers: { + Authorization: `Bearer ${env.RESEND_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + from: env.MAIL_FROM, + to: message.to, + subject: message.subject, + text: message.text, + }), + }); + + if (!response.ok) { + throw new Error(`Email provider failed: ${response.status}`); + } +} +``` + +- [ ] **步骤 4:再次运行测试和 typecheck** + +运行: + +```bash +pnpm test -- tests/auth-config.test.ts +pnpm typecheck +``` + +预期:PASS。 + +- [ ] **步骤 5:提交** + +```bash +git add src/email.ts tests/auth-config.test.ts +git commit -m "feat: add auth email adapter" +``` + +--- + +### 任务 4:启用 OpenAPI 和泄露密码检查 + +**文件:** +- 新建:`src/password.ts` +- 新建:`src/plugins.ts` +- 修改:`src/auth.ts` +- 修改:`tests/auth-worker.test.ts` + +- [ ] **步骤 1:先收紧 OpenAPI 测试** + +把任务 1 中允许 `404` 的测试改成: + +```ts +it("exposes Better Auth OpenAPI reference", async () => { + const response = await worker.fetch(new Request("http://auth.local/api/auth/reference"), env); + expect(response.status).toBe(200); + expect(response.headers.get("content-type") ?? "").toContain("text/html"); +}); +``` + +- [ ] **步骤 2:运行测试,确认失败** + +运行:`pnpm test -- tests/auth-worker.test.ts` + +预期:FAIL,当前 `/api/auth/reference` 不是 200。 + +- [ ] **步骤 3:实现基础插件组合** + +新建 `src/password.ts`: + +```ts +export const passwordPolicy = { + minPasswordLength: 12, + maxPasswordLength: 128, +}; +``` + +新建 `src/plugins.ts`: + +```ts +import type { BetterAuthPlugin } from "better-auth"; +import { haveIBeenPwned, openAPI } from "better-auth/plugins"; +import type { Env } from "./env"; + +export function createAuthPlugins(_env: Env): BetterAuthPlugin[] { + return [ + openAPI(), + haveIBeenPwned({ + customPasswordCompromisedMessage: "This password has appeared in a data breach.", + }), + ]; +} +``` + +修改 `src/auth.ts`: + +```ts +import { betterAuth } from "better-auth"; +import { buildAuthEmail, sendEmail } from "./email"; +import { trustedOrigins, type Env } from "./env"; +import { passwordPolicy } from "./password"; +import { createAuthPlugins } from "./plugins"; + +export function createAuth(env: Env) { + return betterAuth({ + database: env.DB, + secret: env.BETTER_AUTH_SECRET ?? "development-secret-change-before-production", + baseURL: env.BETTER_AUTH_URL, + trustedOrigins: trustedOrigins(env), + emailAndPassword: { + enabled: true, + ...passwordPolicy, + requireEmailVerification: true, + revokeSessionsOnPasswordReset: true, + sendResetPassword: async ({ user, url }) => { + await sendEmail(env, buildAuthEmail({ kind: "reset-password", to: user.email, url })); + }, + }, + emailVerification: { + sendOnSignUp: true, + sendOnSignIn: true, + autoSignInAfterVerification: true, + sendVerificationEmail: async ({ user, url }) => { + await sendEmail(env, buildAuthEmail({ kind: "verify-email", to: user.email, url })); + }, + }, + plugins: createAuthPlugins(env), + }); +} +``` + +- [ ] **步骤 4:运行测试和 typecheck** + +运行: + +```bash +pnpm test -- tests/auth-worker.test.ts tests/auth-config.test.ts +pnpm typecheck +``` + +预期:PASS。 + +- [ ] **步骤 5:提交** + +```bash +git add src/auth.ts src/password.ts src/plugins.ts tests/auth-worker.test.ts +git commit -m "feat: enable auth openapi and password checks" +``` + +--- + +### 任务 5:接入 Captcha 配置 + +**文件:** +- 修改:`src/plugins.ts` +- 修改:`tests/auth-config.test.ts` +- 修改:`wrangler.jsonc` + +- [ ] **步骤 1:先写 captcha 配置测试** + +在 `tests/auth-config.test.ts` 追加: + +```ts +import { createAuthPlugins } from "../src/plugins"; + +describe("auth plugins", () => { + it("does not enable captcha without provider config", () => { + const plugins = createAuthPlugins({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + }); + expect(plugins.map((plugin) => plugin.id)).not.toContain("captcha"); + }); + + it("enables captcha when Cloudflare Turnstile config is present", () => { + const plugins = createAuthPlugins({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + CAPTCHA_PROVIDER: "cloudflare-turnstile", + CAPTCHA_SECRET_KEY: "secret", + }); + expect(plugins.map((plugin) => plugin.id)).toContain("captcha"); + }); +}); +``` + +- [ ] **步骤 2:运行测试,确认失败** + +运行:`pnpm test -- tests/auth-config.test.ts` + +预期:第二条测试 FAIL,插件列表没有 `captcha`。 + +- [ ] **步骤 3:实现 captcha 条件启用** + +修改 `src/plugins.ts`: + +```ts +import { + captcha, + haveIBeenPwned, + openAPI, +} from "better-auth/plugins"; +import type { BetterAuthPlugin } from "better-auth"; +import type { Env } from "./env"; + +export function createAuthPlugins(env: Env): BetterAuthPlugin[] { + const plugins: BetterAuthPlugin[] = [ + openAPI(), + haveIBeenPwned({ + customPasswordCompromisedMessage: "This password has appeared in a data breach.", + }), + ]; + + if (env.CAPTCHA_PROVIDER === "cloudflare-turnstile" && env.CAPTCHA_SECRET_KEY) { + plugins.push( + captcha({ + provider: "cloudflare-turnstile", + secretKey: env.CAPTCHA_SECRET_KEY, + endpoints: ["/sign-up/email", "/sign-in/email", "/forget-password"], + }), + ); + } + + return plugins; +} +``` + +修改 `wrangler.jsonc` 的 `vars`: + +```jsonc +"vars": { + "BETTER_AUTH_URL": "http://localhost:8788", + "TRUSTED_ORIGINS": "http://localhost:8787", + "MAIL_PROVIDER": "resend", + "MAIL_FROM": "noreply@example.com", + "CAPTCHA_PROVIDER": "cloudflare-turnstile" +} +``` + +- [ ] **步骤 4:再次运行测试和 typecheck** + +运行: + +```bash +pnpm test -- tests/auth-config.test.ts +pnpm typecheck +``` + +预期:PASS。 + +- [ ] **步骤 5:提交** + +```bash +git add src/plugins.ts tests/auth-config.test.ts wrangler.jsonc +git commit -m "feat: configure auth captcha plugin" +``` + +--- + +### 任务 6:启用 Email OTP 无密码登录 + +**文件:** +- 修改:`src/plugins.ts` +- 修改:`src/email.ts` +- 修改:`tests/auth-config.test.ts` + +- [ ] **步骤 1:先写 Email OTP 测试** + +在 `tests/auth-config.test.ts` 追加: + +```ts +it("enables email otp plugin by default", () => { + const plugins = createAuthPlugins({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + }); + expect(plugins.map((plugin) => plugin.id)).toContain("email-otp"); +}); +``` + +- [ ] **步骤 2:运行测试,确认失败** + +运行:`pnpm test -- tests/auth-config.test.ts` + +预期:FAIL,插件列表没有 `email-otp`。 + +- [ ] **步骤 3:实现 Email OTP** + +修改 `src/plugins.ts`: + +```ts +import { + captcha, + emailOTP, + haveIBeenPwned, + openAPI, +} from "better-auth/plugins"; +import type { BetterAuthPlugin } from "better-auth"; +import { buildAuthEmail, sendEmail } from "./email"; +import type { Env } from "./env"; + +export function createAuthPlugins(env: Env): BetterAuthPlugin[] { + const plugins: BetterAuthPlugin[] = [ + openAPI(), + haveIBeenPwned({ + customPasswordCompromisedMessage: "This password has appeared in a data breach.", + }), + emailOTP({ + otpLength: 6, + expiresIn: 300, + allowedAttempts: 3, + storeOTP: "hashed", + sendVerificationOnSignUp: true, + overrideDefaultEmailVerification: false, + sendVerificationOTP: async ({ email, otp }) => { + await sendEmail(env, buildAuthEmail({ kind: "email-otp", to: email, otp })); + }, + }), + ]; + + if (env.CAPTCHA_PROVIDER === "cloudflare-turnstile" && env.CAPTCHA_SECRET_KEY) { + plugins.push( + captcha({ + provider: "cloudflare-turnstile", + secretKey: env.CAPTCHA_SECRET_KEY, + endpoints: ["/sign-up/email", "/sign-in/email", "/forget-password"], + }), + ); + } + + return plugins; +} +``` + +- [ ] **步骤 4:再次运行测试和 typecheck** + +运行: + +```bash +pnpm test -- tests/auth-config.test.ts +pnpm typecheck +``` + +预期:PASS。 + +- [ ] **步骤 5:提交** + +```bash +git add src/plugins.ts src/email.ts tests/auth-config.test.ts +git commit -m "feat: enable email otp login" +``` + +--- + +### 任务 7:增加 OAuth provider 配置边界 + +**文件:** +- 新建:`src/oauth.ts` +- 修改:`src/auth.ts` +- 修改:`tests/auth-config.test.ts` + +- [ ] **步骤 1:先写 OAuth 配置测试** + +在 `tests/auth-config.test.ts` 追加: + +```ts +import { createSocialProviders, createGenericOAuthProviders } from "../src/oauth"; + +describe("oauth config", () => { + it("creates built-in social providers from env", () => { + const providers = createSocialProviders({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + GOOGLE_CLIENT_ID: "google-id", + GOOGLE_CLIENT_SECRET: "google-secret", + GITHUB_CLIENT_ID: "github-id", + GITHUB_CLIENT_SECRET: "github-secret", + }); + expect(Object.keys(providers)).toEqual(["google", "github"]); + }); + + it("creates generic oauth providers from env", () => { + const providers = createGenericOAuthProviders({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + GENERIC_OAUTH_PROVIDER_ID: "workos", + GENERIC_OAUTH_DISCOVERY_URL: "https://example.com/.well-known/openid-configuration", + GENERIC_OAUTH_CLIENT_ID: "client-id", + GENERIC_OAUTH_CLIENT_SECRET: "client-secret", + }); + expect(providers).toEqual([ + { + providerId: "workos", + discoveryUrl: "https://example.com/.well-known/openid-configuration", + clientId: "client-id", + clientSecret: "client-secret", + scopes: ["openid", "email", "profile"], + }, + ]); + }); +}); +``` + +- [ ] **步骤 2:运行测试,确认失败** + +运行:`pnpm test -- tests/auth-config.test.ts` + +预期:FAIL,提示 `../src/oauth` 不存在。 + +- [ ] **步骤 3:实现 OAuth 配置 helper** + +新建 `src/oauth.ts`: + +```ts +import type { Env } from "./env"; + +export function createSocialProviders(env: Env) { + const providers: Record = {}; + + if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) { + providers.google = { + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET, + }; + } + + if (env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET) { + providers.github = { + clientId: env.GITHUB_CLIENT_ID, + clientSecret: env.GITHUB_CLIENT_SECRET, + }; + } + + return providers; +} + +export function createGenericOAuthProviders(env: Env) { + if ( + !env.GENERIC_OAUTH_PROVIDER_ID || + !env.GENERIC_OAUTH_DISCOVERY_URL || + !env.GENERIC_OAUTH_CLIENT_ID || + !env.GENERIC_OAUTH_CLIENT_SECRET + ) { + return []; + } + + return [ + { + providerId: env.GENERIC_OAUTH_PROVIDER_ID, + discoveryUrl: env.GENERIC_OAUTH_DISCOVERY_URL, + clientId: env.GENERIC_OAUTH_CLIENT_ID, + clientSecret: env.GENERIC_OAUTH_CLIENT_SECRET, + scopes: ["openid", "email", "profile"], + }, + ]; +} +``` + +修改 `src/auth.ts` 增加: + +```ts +import { createSocialProviders } from "./oauth"; +``` + +并在 `betterAuth({ ... })` 中加入: + +```ts +socialProviders: createSocialProviders(env), +``` + +修改 `src/plugins.ts`,导入并启用 generic OAuth: + +```ts +import { createGenericOAuthProviders } from "./oauth"; +``` + +在 `return plugins` 之前加入: + +```ts +const genericProviders = createGenericOAuthProviders(env); +if (genericProviders.length > 0) { + plugins.push(genericOAuth({ config: genericProviders })); +} +``` + +同时确保 `genericOAuth` 已从 `better-auth/plugins` 导入。 + +- [ ] **步骤 4:运行测试和 typecheck** + +运行: + +```bash +pnpm test -- tests/auth-config.test.ts +pnpm typecheck +``` + +预期:PASS。若 `socialProviders` 类型要求更窄,给 `createSocialProviders` 添加 Better Auth options 推断类型,不要使用 `any`。 + +- [ ] **步骤 5:提交** + +```bash +git add src/auth.ts src/oauth.ts src/plugins.ts tests/auth-config.test.ts +git commit -m "feat: add oauth provider configuration" +``` + +--- + +### 任务 8:安装并启用 Passkey 独立包 + +**文件:** +- 修改:`package.json` +- 修改:`pnpm-lock.yaml` +- 修改:`src/plugins.ts` +- 修改:`src/env.ts` +- 修改:`tests/auth-config.test.ts` + +- [ ] **步骤 1:安装依赖** + +运行: + +```bash +pnpm add @better-auth/passkey@^1.6.16 +``` + +预期:`package.json` 和 `pnpm-lock.yaml` 更新。 + +- [ ] **步骤 2:确认包导出** + +运行: + +```bash +node -e "import('@better-auth/passkey').then(m => console.log(Object.keys(m).sort().join('\\n')))" +``` + +预期:输出包含 `passkey`。如果导出名不同,按实际导出更新后续代码。 + +- [ ] **步骤 3:先写 Passkey 配置测试** + +在 `tests/auth-config.test.ts` 追加: + +```ts +it("enables passkey when rp config is present", () => { + const plugins = createAuthPlugins({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + PASSKEY_RP_ID: "localhost", + PASSKEY_RP_NAME: "cfw-auth", + PASSKEY_ORIGIN: "http://localhost:8788", + }); + expect(plugins.map((plugin) => plugin.id)).toContain("passkey"); +}); +``` + +- [ ] **步骤 4:运行测试,确认失败** + +运行:`pnpm test -- tests/auth-config.test.ts` + +预期:FAIL,插件列表没有 `passkey`。 + +- [ ] **步骤 5:实现 Passkey 条件启用** + +修改 `src/plugins.ts`,增加: + +```ts +import { passkey } from "@better-auth/passkey"; +``` + +在 `return plugins` 前加入: + +```ts +if (env.PASSKEY_RP_ID && env.PASSKEY_RP_NAME && env.PASSKEY_ORIGIN) { + plugins.push( + passkey({ + rpID: env.PASSKEY_RP_ID, + rpName: env.PASSKEY_RP_NAME, + origin: env.PASSKEY_ORIGIN, + }), + ); +} +``` + +- [ ] **步骤 6:运行测试和 typecheck** + +运行: + +```bash +pnpm test -- tests/auth-config.test.ts +pnpm typecheck +``` + +预期:PASS。 + +- [ ] **步骤 7:提交** + +```bash +git add package.json pnpm-lock.yaml src/plugins.ts tests/auth-config.test.ts +git commit -m "feat: enable passkey auth" +``` + +--- + +### 任务 9:启用账号安全和管理插件 + +**文件:** +- 修改:`src/plugins.ts` +- 修改:`tests/auth-config.test.ts` + +- [ ] **步骤 1:先写安全插件测试** + +在 `tests/auth-config.test.ts` 追加: + +```ts +it("enables account security and admin plugins", () => { + const plugins = createAuthPlugins({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + }); + expect(plugins.map((plugin) => plugin.id)).toEqual( + expect.arrayContaining(["two-factor", "multi-session", "last-login-method", "admin"]), + ); +}); +``` + +- [ ] **步骤 2:运行测试,确认失败** + +运行:`pnpm test -- tests/auth-config.test.ts` + +预期:FAIL,缺少安全插件。 + +- [ ] **步骤 3:实现安全插件** + +修改 `src/plugins.ts` 的导入: + +```ts +import { + admin, + captcha, + emailOTP, + genericOAuth, + haveIBeenPwned, + lastLoginMethod, + multiSession, + openAPI, + twoFactor, +} from "better-auth/plugins"; +import type { BetterAuthPlugin } from "better-auth"; +``` + +在基础插件数组中追加: + +```ts +twoFactor(), +multiSession({ maximumSessions: 10 }), +lastLoginMethod({ storeInDatabase: true }), +admin(), +``` + +- [ ] **步骤 4:运行测试和 typecheck** + +运行: + +```bash +pnpm test -- tests/auth-config.test.ts +pnpm typecheck +``` + +预期:PASS。若 `lastLoginMethod({ storeInDatabase: true })` 触发 schema 迁移要求,保留配置并在任务 11 生成 migration。 + +- [ ] **步骤 5:提交** + +```bash +git add src/plugins.ts tests/auth-config.test.ts +git commit -m "feat: enable account security plugins" +``` + +--- + +### 任务 10:按需启用 JWT/Bearer + +**文件:** +- 修改:`src/plugins.ts` +- 修改:`tests/auth-config.test.ts` + +- [ ] **步骤 1:先写 JWT/Bearer 条件测试** + +在 `tests/auth-config.test.ts` 追加: + +```ts +it("enables jwt and bearer only when requested", () => { + const disabled = createAuthPlugins({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + }); + expect(disabled.map((plugin) => plugin.id)).not.toContain("jwt"); + expect(disabled.map((plugin) => plugin.id)).not.toContain("bearer"); + + const enabled = createAuthPlugins({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + ENABLE_JWT: "true", + ENABLE_BEARER: "true", + }); + expect(enabled.map((plugin) => plugin.id)).toEqual(expect.arrayContaining(["jwt", "bearer"])); +}); +``` + +- [ ] **步骤 2:运行测试,确认失败** + +运行:`pnpm test -- tests/auth-config.test.ts` + +预期:FAIL,JWT/Bearer 未条件启用。 + +- [ ] **步骤 3:实现条件启用** + +修改 `src/plugins.ts` 导入: + +```ts +import { booleanEnv } from "./env"; +``` + +从 `better-auth/plugins` 导入 `bearer` 和 `jwt`。 + +在 `return plugins` 前加入: + +```ts +if (booleanEnv(env.ENABLE_JWT)) { + plugins.push(jwt()); +} + +if (booleanEnv(env.ENABLE_BEARER)) { + plugins.push(bearer()); +} +``` + +- [ ] **步骤 4:运行测试和 typecheck** + +运行: + +```bash +pnpm test -- tests/auth-config.test.ts +pnpm typecheck +``` + +预期:PASS。 + +- [ ] **步骤 5:提交** + +```bash +git add src/plugins.ts tests/auth-config.test.ts +git commit -m "feat: add optional token auth plugins" +``` + +--- + +### 任务 11:生成并验证 D1 migration + +**文件:** +- 新建:`migrations/0001_better_auth_account_center.sql` +- 修改:`package.json` + +- [ ] **步骤 1:增加 migration 脚本** + +在 `package.json` 的 `scripts` 中增加: + +```json +"db:generate": "npx @better-auth/cli generate --config src/auth.ts --output migrations/0001_better_auth_account_center.sql", +"db:apply:local": "wrangler d1 migrations apply cfw-auth-dev --local" +``` + +如果 `@better-auth/cli` 的当前命令参数不同,先运行: + +```bash +npx @better-auth/cli --help +npx @better-auth/cli generate --help +``` + +再按实际 help 输出调整脚本。 + +- [ ] **步骤 2:生成 migration** + +运行: + +```bash +pnpm db:generate +``` + +预期:生成 `migrations/0001_better_auth_account_center.sql`,包含 Better Auth 基础表和已启用插件所需表/列。 + +- [ ] **步骤 3:检查 migration 内容** + +运行: + +```bash +rg -n "user|session|account|verification|two|passkey|lastLoginMethod" migrations/0001_better_auth_account_center.sql +``` + +预期:至少能看到基础账号表、session/account/verification 相关表,以及 2FA、passkey 或 last-login-method 相关 schema。若缺少 passkey schema,回到任务 8 检查插件是否真正启用。 + +- [ ] **步骤 4:应用本地 D1 migration** + +运行: + +```bash +pnpm db:apply:local +``` + +预期:Wrangler 成功应用 migration,没有 SQL 错误。 + +- [ ] **步骤 5:运行完整验证** + +运行: + +```bash +pnpm ready +``` + +预期:typecheck 和测试全部 PASS。 + +- [ ] **步骤 6:提交** + +```bash +git add package.json migrations/0001_better_auth_account_center.sql +git commit -m "feat: add account center d1 migration" +``` + +--- + +### 任务 12:补齐真实 Worker auth flow smoke 测试 + +**文件:** +- 修改:`tests/auth-worker.test.ts` + +- [ ] **步骤 1:先写 OpenAPI schema smoke 测试** + +在 `tests/auth-worker.test.ts` 追加: + +```ts +it("returns generated OpenAPI schema", async () => { + const response = await worker.fetch( + new Request("http://auth.local/api/auth/open-api/generate-schema"), + env, + ); + expect(response.status).toBe(200); + const schema = await response.json(); + expect(schema).toMatchObject({ + openapi: expect.any(String), + info: expect.objectContaining({ + title: expect.any(String), + }), + }); +}); +``` + +- [ ] **步骤 2:运行测试,确认当前结果** + +运行:`pnpm test -- tests/auth-worker.test.ts` + +预期:PASS。如果返回 500,查看响应正文,通常是缺少 D1 binding 或插件配置。测试 env 中可以继续不传 `DB`,但如果 Better Auth 在 schema 生成阶段要求数据库,则为测试添加最小 D1 mock。 + +- [ ] **步骤 3:增加找回密码不泄露账号 smoke 测试** + +在 `tests/auth-worker.test.ts` 追加: + +```ts +it("handles forgot password without exposing account existence", async () => { + const response = await worker.fetch( + new Request("http://auth.local/api/auth/forget-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + Origin: "http://localhost:8787", + }, + body: JSON.stringify({ + email: "missing@example.com", + redirectTo: "http://localhost:8787/reset-password", + }), + }), + env, + ); + expect([200, 400, 403]).toContain(response.status); +}); +``` + +- [ ] **步骤 4:运行完整验证** + +运行: + +```bash +pnpm ready +``` + +预期:PASS。 + +- [ ] **步骤 5:提交** + +```bash +git add tests/auth-worker.test.ts +git commit -m "test: cover auth worker openapi smoke" +``` + +--- + +### 任务 13:最终审查和运行时验证 + +**文件:** +- 修改:必要时更新 `README.md` 或新增 `docs/auth-operations.md` + +- [ ] **步骤 1:检查插件导出和包版本** + +运行: + +```bash +node -e "import('better-auth/plugins').then(m => console.log(Object.keys(m).sort().join('\\n')))" +node -e "import('@better-auth/passkey').then(m => console.log(Object.keys(m).sort().join('\\n')))" +``` + +预期:主插件入口包含已使用的内置插件,Passkey 独立包包含 `passkey`。 + +- [ ] **步骤 2:运行完整验证** + +运行: + +```bash +pnpm ready +pnpm db:apply:local +``` + +预期:全部 PASS。 + +- [ ] **步骤 3:启动本地 Worker** + +运行: + +```bash +pnpm dev +``` + +预期:Wrangler 本地服务启动,通常监听 `http://localhost:8788`。 + +- [ ] **步骤 4:另开终端请求真实端点** + +运行: + +```bash +curl -i http://localhost:8788/api/auth/reference +curl -i http://localhost:8788/api/auth/open-api/generate-schema +``` + +预期:`/api/auth/reference` 返回 200 HTML;OpenAPI schema 返回 200 JSON。`/healthz` 不是账号中心职责,本计划不恢复该自定义端点。 + +- [ ] **步骤 5:停止 dev server 并检查工作区** + +运行: + +```bash +git status --short +``` + +预期:没有未提交的本计划改动。如果有测试快照、lockfile 或文档改动,确认后提交。 + +- [ ] **步骤 6:最终提交** + +```bash +git add README.md docs/auth-operations.md package.json pnpm-lock.yaml wrangler.jsonc +git commit -m "docs: document auth account center operations" +``` + +如果没有文档或配置剩余改动,跳过此提交。 diff --git a/package.json b/package.json index 6687434..aa6a0ad 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "ready": "pnpm typecheck && pnpm test" }, "dependencies": { + "@better-auth/passkey": "^1.6.16", "better-auth": "^1.6.0", "hono": "^4.8.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bd6271..349694d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@better-auth/passkey': + specifier: ^1.6.16 + version: 1.6.16(@better-auth/core@1.6.16(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(@cloudflare/workers-types@4.20260610.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(better-auth@1.6.16(@cloudflare/workers-types@4.20260610.1)(vitest@3.2.6))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0) better-auth: specifier: ^1.6.0 version: 1.6.16(@cloudflare/workers-types@4.20260610.1)(vitest@3.2.6) @@ -83,6 +86,16 @@ packages: mongodb: optional: true + '@better-auth/passkey@1.6.16': + resolution: {integrity: sha512-UpDcQmu4fNCHtQ/maFjsdbfeLPu1rzlTSqfpAUhMYGFK/wzHdA1TqWvimmOgAYBbv+mIv5IGOQ6pTC4gzJJj2Q==} + peerDependencies: + '@better-auth/core': ^1.6.16 + '@better-auth/utils': 0.4.1 + '@better-fetch/fetch': 1.2.2 + better-auth: ^1.6.16 + better-call: 1.3.6 + nanostores: ^1.0.1 + '@better-auth/prisma-adapter@1.6.16': resolution: {integrity: sha512-2bIlA7wjBx+4N2QcM32xL/YojRuJpDvskXqT/dGYKToDIEl/7yr12cLYlqeaFLL0O0s5qNZ8jbDtlCz20eogeQ==} peerDependencies: @@ -474,6 +487,9 @@ packages: cpu: [x64] os: [win32] + '@hexagon/base64@1.1.28': + resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -637,6 +653,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@levischuck/tiny-cbor@0.2.11': + resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + '@noble/ciphers@2.2.0': resolution: {integrity: sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==} engines: {node: '>= 20.19.0'} @@ -649,6 +668,46 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} + '@peculiar/asn1-android@2.7.0': + resolution: {integrity: sha512-iD3VskhVQnM4nE3PN9cBdPTR7JrqZy3FYk+uD2CeG6DUqKoANqaEfx0f7izPmW+Qm5JBM35ek+viLCmjy18ByQ==} + + '@peculiar/asn1-cms@2.7.0': + resolution: {integrity: sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ==} + + '@peculiar/asn1-csr@2.7.0': + resolution: {integrity: sha512-VVsAyGqErT9D1SY4aEqozThXMVI+ssVRiv2DDeYuvpBKLIgZ3hYs3Ay3u/VSoKq6ESFi9cf6rf3IOOzfwh7oMA==} + + '@peculiar/asn1-ecc@2.7.0': + resolution: {integrity: sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw==} + + '@peculiar/asn1-pfx@2.7.0': + resolution: {integrity: sha512-V/nrlQVmhg7lYAsM7E13UDL5erAwFv6kCIVFqNaMIHSVi7dngcT839JkRTkQBqznMG98l2XjxYk74ZztAohZzA==} + + '@peculiar/asn1-pkcs8@2.7.0': + resolution: {integrity: sha512-9GTl1nE8Mx1kTZ+7QyYatDyKsm34QcWRBFkY1iPvWC3X4Dona5s/tlLiQsx5WzVdZqiMBZNYT0buyw4/vbhnjw==} + + '@peculiar/asn1-pkcs9@2.7.0': + resolution: {integrity: sha512-Bh7m+OuIaSEllPQcSd9OSp93F4ROWH7sbITWV8MI+8dwsjE5111/87VxiWVvYFKyww3vp39geLv9ENqhwWHcew==} + + '@peculiar/asn1-rsa@2.7.0': + resolution: {integrity: sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ==} + + '@peculiar/asn1-schema@2.7.0': + resolution: {integrity: sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==} + + '@peculiar/asn1-x509-attr@2.7.0': + resolution: {integrity: sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w==} + + '@peculiar/asn1-x509@2.7.0': + resolution: {integrity: sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==} + + '@peculiar/utils@2.0.3': + resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} + + '@peculiar/x509@1.14.3': + resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} + engines: {node: '>=20.0.0'} + '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -796,6 +855,13 @@ packages: cpu: [x64] os: [win32] + '@simplewebauthn/browser@13.3.0': + resolution: {integrity: sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==} + + '@simplewebauthn/server@13.3.1': + resolution: {integrity: sha512-GV/oM/qeycWn8p42JZIMJBsXWQcNFg+nJFzeQTnMA4gN8mXg0+HZFWJerHg8ZN/zlveMS3iV1wzuFpOVWS/46w==} + engines: {node: '>=20.0.0'} + '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} @@ -844,6 +910,10 @@ packages: '@vitest/utils@3.2.6': resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + asn1js@3.0.10: + resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} + engines: {node: '>=12.0.0'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1056,6 +1126,16 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.5: + resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} + engines: {node: '>=16.0.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + rollup@4.61.1: resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1118,9 +1198,16 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsyringe@4.10.0: + resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} + engines: {node: '>= 6.0.0'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1285,6 +1372,18 @@ snapshots: '@better-auth/core': 1.6.16(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(@cloudflare/workers-types@4.20260610.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.1 + '@better-auth/passkey@1.6.16(@better-auth/core@1.6.16(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(@cloudflare/workers-types@4.20260610.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(better-auth@1.6.16(@cloudflare/workers-types@4.20260610.1)(vitest@3.2.6))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0)': + dependencies: + '@better-auth/core': 1.6.16(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(@cloudflare/workers-types@4.20260610.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.1 + '@better-fetch/fetch': 1.2.2 + '@simplewebauthn/browser': 13.3.0 + '@simplewebauthn/server': 13.3.1 + better-auth: 1.6.16(@cloudflare/workers-types@4.20260610.1)(vitest@3.2.6) + better-call: 1.3.6(zod@4.4.3) + nanostores: 1.3.0 + zod: 4.4.3 + '@better-auth/prisma-adapter@1.6.16(@better-auth/core@1.6.16(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(@cloudflare/workers-types@4.20260610.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)': dependencies: '@better-auth/core': 1.6.16(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(@cloudflare/workers-types@4.20260610.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) @@ -1492,6 +1591,8 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true + '@hexagon/base64@1.1.28': {} + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -1597,12 +1698,114 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@levischuck/tiny-cbor@0.2.11': {} + '@noble/ciphers@2.2.0': {} '@noble/hashes@2.2.0': {} '@opentelemetry/semantic-conventions@1.41.1': {} + '@peculiar/asn1-android@2.7.0': + dependencies: + '@peculiar/asn1-schema': 2.7.0 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-cms@2.7.0': + dependencies: + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/asn1-x509': 2.7.0 + '@peculiar/asn1-x509-attr': 2.7.0 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-csr@2.7.0': + dependencies: + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/asn1-x509': 2.7.0 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.7.0': + dependencies: + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/asn1-x509': 2.7.0 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-pfx@2.7.0': + dependencies: + '@peculiar/asn1-cms': 2.7.0 + '@peculiar/asn1-pkcs8': 2.7.0 + '@peculiar/asn1-rsa': 2.7.0 + '@peculiar/asn1-schema': 2.7.0 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs8@2.7.0': + dependencies: + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/asn1-x509': 2.7.0 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs9@2.7.0': + dependencies: + '@peculiar/asn1-cms': 2.7.0 + '@peculiar/asn1-pfx': 2.7.0 + '@peculiar/asn1-pkcs8': 2.7.0 + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/asn1-x509': 2.7.0 + '@peculiar/asn1-x509-attr': 2.7.0 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.7.0': + dependencies: + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/asn1-x509': 2.7.0 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.7.0': + dependencies: + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-x509-attr@2.7.0': + dependencies: + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/asn1-x509': 2.7.0 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.7.0': + dependencies: + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/utils@2.0.3': + dependencies: + tslib: 2.8.1 + + '@peculiar/x509@1.14.3': + dependencies: + '@peculiar/asn1-cms': 2.7.0 + '@peculiar/asn1-csr': 2.7.0 + '@peculiar/asn1-ecc': 2.7.0 + '@peculiar/asn1-pkcs9': 2.7.0 + '@peculiar/asn1-rsa': 2.7.0 + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/asn1-x509': 2.7.0 + pvtsutils: 1.3.6 + reflect-metadata: 0.2.2 + tslib: 2.8.1 + tsyringe: 4.10.0 + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -1690,6 +1893,19 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.61.1': optional: true + '@simplewebauthn/browser@13.3.0': {} + + '@simplewebauthn/server@13.3.1': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.11 + '@peculiar/asn1-android': 2.7.0 + '@peculiar/asn1-ecc': 2.7.0 + '@peculiar/asn1-rsa': 2.7.0 + '@peculiar/asn1-schema': 2.7.0 + '@peculiar/asn1-x509': 2.7.0 + '@peculiar/x509': 1.14.3 + '@sindresorhus/is@7.2.0': {} '@speed-highlight/core@1.2.16': {} @@ -1747,6 +1963,12 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + asn1js@3.0.10: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + assertion-error@2.0.1: {} better-auth@1.6.16(@cloudflare/workers-types@4.20260610.1)(vitest@3.2.6): @@ -1934,6 +2156,14 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.5: {} + + reflect-metadata@0.2.2: {} + rollup@4.61.1: dependencies: '@types/estree': 1.0.9 @@ -2031,8 +2261,13 @@ snapshots: tinyspy@4.0.4: {} - tslib@2.8.1: - optional: true + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsyringe@4.10.0: + dependencies: + tslib: 1.14.1 typescript@5.9.3: {} diff --git a/tests/auth-config.test.ts b/tests/auth-config.test.ts index 430110e..de5cdaf 100644 --- a/tests/auth-config.test.ts +++ b/tests/auth-config.test.ts @@ -134,3 +134,16 @@ describe("oauth config", () => { ]); }); }); + +describe("passkey config", () => { + it("enables passkey when rp config is present", () => { + const plugins = createAuthPlugins({ + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + PASSKEY_RP_ID: "localhost", + PASSKEY_RP_NAME: "cfw-auth", + PASSKEY_ORIGIN: "http://localhost:8788", + }); + expect(plugins.map((plugin) => plugin.id)).toContain("passkey"); + }); +});