关于 heartwood-auth
heartwood-auth 是面向 Grove 应用开发者的认证集成技能,涵盖 Heartwood 登录、会话验证与路由保护。它提供 Better Auth、跨子域 Cookie SSO 和兼容旧版令牌流程的接入指引。
主要功能
- 01接入 Better Auth 客户端
- 02配置跨子域 Cookie SSO
- 03兼容旧版 OAuth 令牌流程
- 04验证会话并保护路由
- 05管理客户端凭据与认证错误
适用场景
- 01为 Grove 应用添加登录认证
- 02保护管理页面和 API 路由
- 03在受保护请求中验证用户会话
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ autumnsgrove/ lattice --skill heartwood-auth安装后在 Claude Code 中输入/heartwood-auth确认已加载。
详细介绍
Heartwood 身份验证集成技能
适用场景
在以下情况下启用此技能:
- 为 Grove 应用添加身份验证
- 保护管理路由
- 验证用户会话
- 设置 OAuth 登录
- 与 Heartwood(GroveAuth)集成
概览
Heartwood 是 Grove 基于 Better Auth 构建的集中式身份验证服务。
| 域名 | 用途 |
|---|---|
heartwood.grove.place | 前端(登录界面) |
auth-api.grove.place | 后端 API |
主要功能
- OAuth 提供商:Google
- 魔法链接:通过 Resend 发送点击即可登录的邮件
- 通行密钥:基于 WebAuthn 的无密码身份验证
- KV 缓存会话:验证耗时不到 100 毫秒
- 跨子域 SSO:在所有 .grove.place 上共用单个会话
集成方式
方案 A:Better Auth 客户端(推荐)
对于新的集成,请使用 Better Auth 的客户端库:
// src/lib/auth/client.ts
import { createAuthClient } from "better-auth/client";
export const auth = createAuthClient({
baseURL: "https://auth-api.grove.place",
});
// Sign in with Google
await auth.signIn.social({ provider: "google" });
// Get current session
const session = await auth.getSession();
// Sign out
await auth.signOut();
方案 B:基于 Cookie 的 SSO(*.grove.place 应用)
对于 .grove.place 子域上的应用,会话会通过 Cookie 自动生效:
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
// Check session via Heartwood API
const sessionCookie = event.cookies.get("better-auth.session_token");
if (sessionCookie) {
try {
const response = await fetch("https://auth-api.grove.place/api/auth/session", {
headers: {
Cookie: `better-auth.session_token=${sessionCookie}`,
},
});
if (response.ok) {
const data = await response.json();
event.locals.user = data.user;
event.locals.session = data.session;
}
} catch {
// Session invalid, expired, or network error — silently continue
}
}
return resolve(event);
};
方案 C:旧版令牌流程(向后兼容)
对于使用旧版 OAuth 流程的现有集成:
// 1. Redirect to Heartwood login
const params = new URLSearchParams({
client_id: "your-client-id",
redirect_uri: "https://yourapp.grove.place/auth/callback",
state: crypto.randomUUID(),
code_challenge: await generateCodeChallenge(verifier),
code_challenge_method: "S256",
});
redirect(302, `https://auth-api.grove.place/login?${params}`);
// 2. Exchange code for tokens (in callback route)
const tokens = await fetch("https://auth-api.grove.place/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: "https://yourapp.grove.place/auth/callback",
client_id: "your-client-id",
client_secret: env.HEARTWOOD_CLIENT_SECRET,
code_verifier: verifier,
}),
}).then((r) => r.json());
// 3. Verify token on protected routes
const user = await fetch("https://auth-api.grove.place/verify", {
headers: { Authorization: `Bearer ${tokens.access_token}` },
}).then((r) => r.json());
受保护路由模式
SvelteKit 布局保护
// src/routes/admin/+layout.server.ts
import { redirect } from "@sveltejs/kit";
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) {
throw redirect(302, "/auth/login");
}
return {
user: locals.user,
};
};
API 路由保护
// src/routes/api/protected/+server.ts
import { json, error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) {
throw error(401, "Unauthorized");
}
return json({ message: "Protected data", user: locals.user });
};
会话验证
通过 Better Auth 会话端点
async function validateSession(sessionToken: string) {
const response = await fetch("https://auth-api.grove.place/api/auth/session", {
headers: {
Cookie: `better-auth.session_token=${sessionToken}`,
},
});
if (!response.ok) return null;
const data = await response.json();
return data.session ? data : null;
}
通过旧版验证端点
async function validateToken(accessToken: string) {
const response = await fetch("https://auth-api.grove.place/verify", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const data = await response.json();
return data.active ? data : null;
}
客户端注册
要将新应用与 Heartwood 集成,需要将其注册为客户端。
1. 生成客户端凭据
# Generate a secure client secret
openssl rand -base64 32
# Example: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
# Hash it for storage (base64url encoding)
echo -n "YOUR_SECRET" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '='
2. 在 Heartwood 数据库中注册
INSERT INTO clients (id, name, client_id, client_secret_hash, redirect_uris, allowed_origins)
VALUES (
lower(hex(randomblob(16))),
'Your App Name',
'your-app-id',
'BASE64URL_HASHED_SECRET',
'["https://yourapp.grove.place/auth/callback"]',
'["https://yourapp.grove.place"]'
);
3. 在应用中设置密钥
# Set the client secret on your app
wrangler secret put HEARTWOOD_CLIENT_SECRET
# Paste: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
环境变量
| 变量 | 描述 |
|---|---|
HEARTWOOD_CLIENT_ID | 已注册的客户端 ID |
HEARTWOOD_CLIENT_SECRET | 客户端密钥(切勿提交!) |
API 端点参考
Better Auth 端点(推荐)
| 方法 | 端点 | 用途 |
|---|---|---|
| POST | /api/auth/sign-in/social | OAuth 登录 |
| GET | /api/auth/session | 获取当前会话 |
| POST | /api/auth/sign-out | 登出 |
旧版端点
| 方法 | 端点 | 用途 |
|---|---|---|
| GET | /login | 登录页面 |
| POST | /token | 将授权码换成令牌 |
| GET | /verify | 验证访问令牌 |
| GET | /userinfo | 获取用户信息 |
最佳实践
应当
- 新集成使用 Better Auth 客户端
- 对每个受保护的请求都验证会话
- 使用
httpOnlyCookie 存储令牌 - 妥善处理身份验证失败的情况
- 会话过期时平稳地将用户登出
不应
- 将令牌存储在 localStorage 中(容易受到 XSS 攻击)
- 跳过 API 路由上的会话验证
- 将客户端密钥硬编码
- 忽略令牌过期
类型安全的错误处理
在 catch 代码块中使用 Rootwork 类型守卫,而不是手动收窄错误类型。从 @autumnsgrove/lattice/server 导入:
import { isRedirect, isHttpError } from "@autumnsgrove/lattice/server";
try {
// ... auth flow
} catch (err) {
if (isRedirect(err)) throw err; // Re-throw SvelteKit redirects
if (isHttpError(err)) {
// Handle HTTP errors with proper status code
console.error(`Auth failed: ${err.status} ${err.body}`);
}
// Fallback error handling
}
从 KV/缓存读取会话数据: 使用 safeJsonParse() 进行类型安全的反序列化:
import { safeJsonParse } from "@autumnsgrove/lattice/server";
import { z } from "zod";
const sessionSchema = z.object({
userId: z.string(),
email: z.string().email(),
});
const rawSession = await kv.get("session:123");
const session = safeJsonParse(rawSession, sessionSchema);
if (session) {
event.locals.user = { id: session.userId, email: session.email };
}
跨子域 SSO
所有 .grove.place 应用都会自动共用同一个会话 Cookie:
better-auth.session_token (domain=.grove.place)
用户只要在任意 Grove 服务上登录,就会在所有服务上保持登录状态。
故障排除
“未找到会话”错误
- 检查 Cookie 域是否为
.grove.place - 确认 SESSION_KV 命名空间可访问
- 检查会话是否已过期
OAuth 回调错误
- 确认 redirect_uri 与已注册的客户端匹配
- 检查 client_id 是否正确
- 确保 client_secret_hash 使用 base64url 编码
认证速度慢
- 确保已启用 KV 缓存(SESSION_KV 绑定)
- 检查是否存在冷启动问题(Workers 可能会休眠)
错误代码(HW-AUTH 目录)
Heartwood 有自己的 Signpost 错误目录,共包含 16 个代码:
import {
AUTH_ERRORS,
getAuthError,
logAuthError,
buildErrorParams,
} from "@autumnsgrove/lattice/heartwood";
关键错误代码:
| Code | Key | When |
|---|---|---|
HW-AUTH-001 | ACCESS_DENIED | 用户没有权限 |
HW-AUTH-002 | PROVIDER_ERROR | OAuth 提供方失败 |
HW-AUTH-004 | REDIRECT_URI_MISMATCH | 回调 URL 与已注册的客户端不匹配 |
HW-AUTH-020 | NO_SESSION | 未找到会话 Cookie |
HW-AUTH-021 | SESSION_EXPIRED | 会话已超时 |
HW-AUTH-022 | INVALID_TOKEN | 令牌验证失败 |
HW-AUTH-023 | TOKEN_EXCHANGE_FAILED | 授权码换取令牌失败 |
将 OAuth 错误映射到 Signpost 代码:
// In callback handler — map OAuth error param to structured error
const authError = getAuthError(errorParam); // e.g. "access_denied" → AUTH_ERRORS.ACCESS_DENIED
logAuthError(authError, { path: "/auth/callback", ip });
redirect(302, `/login?${buildErrorParams(authError)}`);
**编号范围:**001-019 为基础设施,020-039 为会话/令牌,040+ 为预留。
完整的 Signpost 参考文档请参阅 AgentUsage/error_handling.md。
相关资源
- Heartwood 规范:
/Users/autumn/Documents/Projects/GroveAuth/GROVEAUTH_SPEC.md - Better Auth 文档:https://better-auth.com
- 客户端设置指南:
/Users/autumn/Documents/Projects/GroveAuth/docs/OAUTH_CLIENT_SETUP.md
评论
支持 Markdown