重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
heartwood-auth by autumnsgrove/groveengine
npx skills add https://github.com/autumnsgrove/groveengine --skill heartwood-auth在以下情况下激活此技能:
Heartwood 是 Grove 由 Better Auth 驱动的集中式身份验证服务。
| 域 | 用途 |
|---|---|
heartwood.grove.place | 前端(登录界面) |
auth-api.grove.place | 后端 API |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
对于新的集成,使用 Better Auth 的客户端库:
// src/lib/auth/client.ts
import { createAuthClient } from "better-auth/client";
export const auth = createAuthClient({
baseURL: "https://auth-api.grove.place",
});
// 使用 Google 登录
await auth.signIn.social({ provider: "google" });
// 获取当前会话
const session = await auth.getSession();
// 登出
await auth.signOut();
对于在 .grove.place 子域上的应用,会话通过 Cookie 自动工作:
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
// 通过 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 {
// 会话无效、过期或网络错误 — 静默继续
}
}
return resolve(event);
};
对于使用旧版 OAuth 流程的现有集成:
// 1. 重定向到 Heartwood 登录
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. 用授权码交换令牌(在回调路由中)
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. 在受保护路由上验证令牌
const user = await fetch("https://auth-api.grove.place/verify", {
headers: { Authorization: `Bearer ${tokens.access_token}` },
}).then((r) => r.json());
// 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,
};
};
// 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, "未经授权");
}
return json({ message: "受保护的数据", user: locals.user });
};
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 集成,您需要将其注册为客户端。
# 生成安全的客户端密钥
openssl rand -base64 32
# 示例: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
# 为存储对其进行哈希处理(base64url 编码)
echo -n "YOUR_SECRET" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '='
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"]'
);
# 在您的应用上设置客户端密钥
wrangler secret put HEARTWOOD_CLIENT_SECRET
# 粘贴: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
| 变量 | 描述 |
|---|---|
HEARTWOOD_CLIENT_ID | 您注册的客户端 ID |
HEARTWOOD_CLIENT_SECRET | 您的客户端密钥(切勿提交!) |
| 方法 | 端点 | 用途 |
|---|---|---|
| POST | /api/auth/sign-in/social | OAuth 登录 |
| POST | /api/auth/sign-in/magic-link | 魔法链接登录 |
| POST | /api/auth/sign-in/passkey | 通行密钥登录 |
| GET | /api/auth/session | 获取当前会话 |
| POST | /api/auth/sign-out | 登出 |
| 方法 | 端点 | 用途 |
|---|---|---|
| GET | /login | 登录页面 |
| POST | /token | 用授权码交换令牌 |
| GET | /verify | 验证访问令牌 |
| GET | /userinfo | 获取用户信息 |
httpOnly Cookie 存储令牌在 catch 块中使用 Rootwork 类型守卫,而不是手动进行错误类型收窄。从 @autumnsgrove/lattice/server 导入:
import { isRedirect, isHttpError } from "@autumnsgrove/lattice/server";
try {
// ... 身份验证流程
} catch (err) {
if (isRedirect(err)) throw err; // 重新抛出 SvelteKit 重定向
if (isHttpError(err)) {
// 使用正确的状态码处理 HTTP 错误
console.error(`身份验证失败: ${err.status} ${err.body}`);
}
// 后备错误处理
}
从 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 };
}
所有 .grove.place 应用自动共享相同的会话 Cookie:
better-auth.session_token (domain=.grove.place)
一旦用户在任何一个 Grove 属性上登录,他们就在所有地方都已登录。
.grove.placeHeartwood 拥有自己的 Signpost 错误目录,包含 16 个代码:
import {
AUTH_ERRORS,
getAuthError,
logAuthError,
buildErrorParams,
} from "@autumnsgrove/lattice/heartwood";
关键错误代码:
| 代码 | 键 | 何时发生 |
|---|---|---|
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 代码:
// 在回调处理程序中 — 将 OAuth 错误参数映射到结构化错误
const authError = getAuthError(errorParam); // 例如 "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。
/Users/autumn/Documents/Projects/GroveAuth/GROVEAUTH_SPEC.md/Users/autumn/Documents/Projects/GroveAuth/docs/OAUTH_CLIENT_SETUP.md每周安装数
50
代码仓库
GitHub 星标数
2
首次出现
2026年2月5日
安全审计
安装于
opencode50
codex50
gemini-cli50
continue49
cursor49
codebuddy49
Activate this skill when:
Heartwood is Grove's centralized authentication service powered by Better Auth.
| Domain | Purpose |
|---|---|
heartwood.grove.place | Frontend (login UI) |
auth-api.grove.place | Backend API |
For new integrations, use Better Auth's client library:
// 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();
For apps on .grove.place subdomains, sessions work automatically via cookies:
// 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);
};
For existing integrations using the legacy OAuth flow:
// 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());
// 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,
};
};
// 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 });
};
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;
}
To integrate a new app with Heartwood, you need to register it as a client.
# 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 '='
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"]'
);
# Set the client secret on your app
wrangler secret put HEARTWOOD_CLIENT_SECRET
# Paste: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
| Variable | Description |
|---|---|
HEARTWOOD_CLIENT_ID | Your registered client ID |
HEARTWOOD_CLIENT_SECRET | Your client secret (never commit!) |
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/auth/sign-in/social | OAuth sign-in |
| POST | /api/auth/sign-in/magic-link | Magic link sign-in |
| POST | /api/auth/sign-in/passkey | Passkey sign-in |
| GET | /api/auth/session | Get current session |
| POST | /api/auth/sign-out |
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /login | Login page |
| POST | /token | Exchange code for tokens |
| GET | /verify | Validate access token |
| GET | /userinfo | Get user info |
httpOnly cookies for token storageUse Rootwork type guards in catch blocks instead of manual error type narrowing. Import from @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
}
Reading session data from KV/cache: Use safeJsonParse() for type-safe deserialization:
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 };
}
All .grove.place apps share the same session cookie automatically:
better-auth.session_token (domain=.grove.place)
Once a user signs in on any Grove property, they're signed in everywhere.
.grove.placeHeartwood has its own Signpost error catalog with 16 codes:
import {
AUTH_ERRORS,
getAuthError,
logAuthError,
buildErrorParams,
} from "@autumnsgrove/lattice/heartwood";
Key error codes:
| Code | Key | When |
|---|---|---|
HW-AUTH-001 | ACCESS_DENIED | User lacks permission |
HW-AUTH-002 | PROVIDER_ERROR | OAuth provider failed |
HW-AUTH-004 | REDIRECT_URI_MISMATCH | Callback URL doesn't match registered client |
HW-AUTH-020 |
Mapping OAuth errors to Signpost codes:
// 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)}`);
Number ranges: 001-019 infrastructure, 020-039 session/token, 040+ reserved.
See AgentUsage/error_handling.md for the full Signpost reference.
/Users/autumn/Documents/Projects/GroveAuth/GROVEAUTH_SPEC.md/Users/autumn/Documents/Projects/GroveAuth/docs/OAUTH_CLIENT_SETUP.mdWeekly Installs
50
Repository
GitHub Stars
2
First Seen
Feb 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykFail
Installed on
opencode50
codex50
gemini-cli50
continue49
cursor49
codebuddy49
lark-cli 共享规则:飞书资源操作指南与权限配置详解
41,800 周安装
SQL性能优化助手 - 专业SQL查询调优与索引策略优化工具
9,200 周安装
Conventional Commit规范提交助手 - GitHub Copilot自动生成标准化提交信息
9,100 周安装
Defuddle CLI:网页内容提取工具,一键移除广告导航,节省AI令牌使用量
9,600 周安装
网站设计审查工具 - 自动检测并修复HTML/CSS/JS、React、Vue等框架的视觉与布局问题
9,300 周安装
UnoCSS 即时原子化 CSS 引擎:灵活可扩展,Tailwind CSS 超集,前端开发必备
9,300 周安装
网站性能优化指南:Lighthouse审计、核心Web指标与性能预算实践
9,300 周安装
| Sign out |
NO_SESSION |
| No session cookie found |
HW-AUTH-021 | SESSION_EXPIRED | Session timed out |
HW-AUTH-022 | INVALID_TOKEN | Token verification failed |
HW-AUTH-023 | TOKEN_EXCHANGE_FAILED | Code-for-token exchange failed |