heartwood-auth
1
总安装量
1
周安装量
#42083
全站排名
安装命令
npx skills add https://github.com/autumnsgrove/groveengine --skill heartwood-auth
Agent 安装分布
replit
1
opencode
1
codex
1
claude-code
1
gemini-cli
1
Skill 文档
Heartwood Auth Integration Skill
When to Activate
Activate this skill when:
- Adding authentication to a Grove application
- Protecting admin routes
- Validating user sessions
- Setting up OAuth sign-in
- Integrating with Heartwood (GroveAuth)
Overview
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 |
Key Features
- OAuth Providers: Google
- Magic Links: Click-to-login emails via Resend
- Passkeys: WebAuthn passwordless authentication
- KV-Cached Sessions: Sub-100ms validation
- Cross-Subdomain SSO: Single session across all .grove.place
Integration Approaches
Option A: Better Auth Client (Recommended)
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();
Option B: Cookie-Based SSO (*.grove.place apps)
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 or expired
}
}
return resolve(event);
};
Option C: Legacy Token Flow (Backwards Compatible)
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());
Protected Routes Pattern
SvelteKit Layout Protection
// 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 Route Protection
// 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 });
};
Session Validation
Via Better Auth Session Endpoint
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;
}
Via Legacy Verify Endpoint
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;
}
Client Registration
To integrate a new app with Heartwood, you need to register it as a client.
1. Generate Client Credentials
# 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. Register in Heartwood Database
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 Secrets on Your App
# Set the client secret on your app
wrangler secret put HEARTWOOD_CLIENT_SECRET
# Paste: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
Environment Variables
| Variable | Description |
|---|---|
HEARTWOOD_CLIENT_ID |
Your registered client ID |
HEARTWOOD_CLIENT_SECRET |
Your client secret (never commit!) |
API Endpoints Reference
Better Auth Endpoints (Recommended)
| 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 |
Sign out |
Legacy Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /login |
Login page |
| POST | /token |
Exchange code for tokens |
| GET | /verify |
Validate access token |
| GET | /userinfo |
Get user info |
Best Practices
DO
- Use Better Auth client for new integrations
- Validate sessions on every protected request
- Use
httpOnlycookies for token storage - Implement proper error handling for auth failures
- Log out users gracefully when sessions expire
DON’T
- Store tokens in localStorage (XSS vulnerable)
- Skip session validation on API routes
- Hardcode client secrets
- Ignore token expiration
Cross-Subdomain SSO
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.
Troubleshooting
“Session not found” errors
- Check cookie domain is
.grove.place - Verify SESSION_KV namespace is accessible
- Check session hasn’t expired
OAuth callback errors
- Verify redirect_uri matches registered client
- Check client_id is correct
- Ensure client_secret_hash uses base64url encoding
Slow authentication
- Ensure KV caching is enabled (SESSION_KV binding)
- Check for cold start issues (Workers may sleep)
Related Resources
- Heartwood Spec:
/Users/autumn/Documents/Projects/GroveAuth/GROVEAUTH_SPEC.md - Better Auth Docs: https://better-auth.com
- Client Setup Guide:
/Users/autumn/Documents/Projects/GroveAuth/docs/OAUTH_CLIENT_SETUP.md